From 9a78f86f773cbf34e29ec51fc06e3f04072c88d0 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Sun, 2 Mar 2008 01:25:00 +0000 Subject: - Support failover between multiple agents - Support table queries - Major refactoring of internals. --- common/Makefile.am | 16 + common/compat.c | 11 +- common/config-parser.c | 85 +++- common/config-parser.h | 8 +- common/hash.c | 19 + common/hash.h | 5 + common/log.c | 59 +++ common/log.h | 58 +++ common/server-mainloop.c | 2 +- common/snmp-engine.c | 1011 ++++++++++++++++++++++++++++++++++++++++++++++ common/snmp-engine.h | 27 ++ 11 files changed, 1292 insertions(+), 9 deletions(-) create mode 100644 common/Makefile.am create mode 100644 common/log.c create mode 100644 common/log.h create mode 100644 common/snmp-engine.c create mode 100644 common/snmp-engine.h (limited to 'common') diff --git a/common/Makefile.am b/common/Makefile.am new file mode 100644 index 0000000..61ef9a4 --- /dev/null +++ b/common/Makefile.am @@ -0,0 +1,16 @@ + +noinst_LIBRARIES = libcommon.a + +libcommon_a_SOURCES = \ + async-resolver.h async-resolver.c \ + config-parser.h config-parser.c \ + compat.h compat.c \ + hash.h hash.c \ + log.h log.c \ + server-mainloop.c server-mainloop.h \ + sock-any.h sock-any.c \ + snmp-engine.h snmp-engine.c \ + usuals.h + +libcommon_a_CFLAGS = -I${top_srcdir}/common/ -I${top_srcdir}/bsnmp/ -I${top_srcdir} + diff --git a/common/compat.c b/common/compat.c index 6f9c58d..53de9bf 100644 --- a/common/compat.c +++ b/common/compat.c @@ -136,15 +136,18 @@ strtob(const char* str) size_t strlcpy(char *dst, const char *src, size_t len) { - size_t ret = strlen(dst); + size_t ret = strlen(src); + size_t copied; - while (len > 1) { + while (ret > 0 && len > 1) { *dst++ = *src++; - len--; + --len; + --ret; + ++copied; } if (len > 0) *dst = '\0'; - return (ret); + return copied; } #endif /* HAVE_STRLCPY */ diff --git a/common/config-parser.c b/common/config-parser.c index cf1599d..f95d44e 100644 --- a/common/config-parser.c +++ b/common/config-parser.c @@ -36,6 +36,10 @@ */ #include "usuals.h" + +#include "config-parser.h" + +#include #include #include #include @@ -46,8 +50,6 @@ #include #include -#include "config-parser.h" - static void errmsg(const char* filename, void* data, const char* msg, ...) { @@ -346,7 +348,8 @@ cfg_parse_dir(const char* dirname, void* data) } const char* -cfg_parse_uri (char *uri, char** scheme, char** host, char** user, char** path) +cfg_parse_uri (char *uri, char** scheme, char** host, char** user, + char** path, char** query) { char* t; @@ -354,6 +357,7 @@ cfg_parse_uri (char *uri, char** scheme, char** host, char** user, char** path) *host = NULL; *user = NULL; *path = NULL; + *query = NULL; *scheme = strsep(&uri, ":"); if(uri == NULL || (uri[0] != '/' && uri[1] != '/')) @@ -384,6 +388,13 @@ cfg_parse_uri (char *uri, char** scheme, char** host, char** user, char** path) while((*path)[0] == '/') (*path)++; + *query = strchr(*path, '?'); + if(*query) + { + *(*query) = 0; + (*query)++; + } + return NULL; } @@ -407,3 +418,71 @@ cfg_parse_scheme(const char *str, enum snmp_version *scheme) return NULL; } +const char* +cfg_parse_query (char *query, char **name, char **value, char **remainder) +{ + const char *msg; + char *x; + + *remainder = NULL; + + if (*query == '&' || *query == '?') + query++; + + /* Only use the first argument */ + x = strchr (query, '&'); + if (x) { + *x = 0; + *remainder = x + 1; + } + + /* Parse into argument and value */ + *value = strchr (query, '='); + if (*value) { + *(*value) = 0; + (*value)++; + msg = cfg_parse_url_decode (*value); + if (msg) + return msg; + } + + *name = query; + return NULL; +} + +const static char HEX_CHARS[] = "0123456789abcdef"; + +const char* +cfg_parse_url_decode (char *value) +{ + char *p, *a, *b; + + /* Change all plusses to spaces */ + for (p = strchr (value, '+'); p; p = strchr (p, '+')) + *p = ' '; + + /* Now loop through looking for escapes */ + p = value; + while (*value) { + /* + * A percent sign followed by two hex digits means + * that the digits represent an escaped character. + */ + if (*value == '%') { + value++; + a = strchr (HEX_CHARS, tolower(value[0])); + b = strchr (HEX_CHARS, tolower(value[1])); + if (a && b) { + *p = (a - HEX_CHARS) << 4; + *(p++) |= (b - HEX_CHARS); + value += 2; + continue; + } + } + + *(p++) = *(value++); + } + + *p = 0; + return NULL; +} diff --git a/common/config-parser.h b/common/config-parser.h index 19a300a..c804874 100644 --- a/common/config-parser.h +++ b/common/config-parser.h @@ -51,9 +51,15 @@ int cfg_parse_dir(const char* dirname, void* data); int cfg_parse_file(const char* filename, void* data, char** memory); /* A helper for parsing URIs */ -const char* cfg_parse_uri (char *uri, char** scheme, char** host, char** user, char** path); +const char* cfg_parse_uri (char *uri, char** scheme, char** host, char** user, char** path, char** query); /* Parsing snmp, snmp2 snmp2c etc... */ const char* cfg_parse_scheme (const char *input, enum snmp_version *scheme); +/* Parse query=xxxx arguments */ +const char* cfg_parse_query (char *query, char **name, char **value, char **remainder); + +/* A helper for URL decoding URI values */ +const char* cfg_parse_url_decode (char *value); + #endif /* __CONFIG_PARSER_H__ */ diff --git a/common/hash.c b/common/hash.c index 9acd2df..512a914 100644 --- a/common/hash.c +++ b/common/hash.c @@ -374,6 +374,25 @@ void* hsh_rem(hsh_t* ht, const void* key, size_t klen) return val; } +void hsh_clear(hsh_t* ht) +{ + hsh_entry_t *he, *next; + int i; + + /* Free all entries in the array */ + for (i = 0; i < ht->max; ++i) { + he = ht->array[i]; + while (he) { + next = he->next; + free (he); + he = next; + } + } + + memset (ht->array, 0, ht->max * sizeof (hsh_entry_t*)); + ht->count = 0; +} + unsigned int hsh_count(hsh_t* ht) { return ht->count; diff --git a/common/hash.h b/common/hash.h index c046b38..30153f6 100644 --- a/common/hash.h +++ b/common/hash.h @@ -142,6 +142,11 @@ hsh_index_t* hsh_next(hsh_index_t* hi); */ void* hsh_this(hsh_index_t* hi, const void** key, size_t* klen); +/* + * hsh_clear: Clear all values from has htable. + */ +void hsh_clear(hsh_t* ht); + /* * This can be passed as 'klen' in any of the above functions to indicate * a string-valued key, and have hash compute the length automatically. diff --git a/common/log.c b/common/log.c new file mode 100644 index 0000000..f6a4338 --- /dev/null +++ b/common/log.c @@ -0,0 +1,59 @@ + +#include "log.h" + +#include +#include + +void +log_error (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_ERR, errno, msg, va); + va_end (va); +} + +void +log_errorx (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_ERR, 0, msg, va); + va_end (va); +} + +void +log_warn (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_WARNING, errno, msg, va); + va_end (va); +} + +void +log_warnx (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_WARNING, 0, msg, va); + va_end (va); +} + +void +log_debug (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_DEBUG, 0, msg, va); + va_end (va); +} + +void +log_info (const char *msg, ...) +{ + va_list va; + va_start (va, msg); + log_vmessage (LOG_INFO, 0, msg, va); + va_end (va); +} diff --git a/common/log.h b/common/log.h new file mode 100644 index 0000000..f399328 --- /dev/null +++ b/common/log.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2008, Stefan Walter + * 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 + * Stef Walter + * + */ + +#ifndef LOG_H_ +#define LOG_H_ + +#include + +void log_error (const char *msg, ...); + +void log_errorx (const char *msg, ...); + +void log_warn (const char *msg, ...); + +void log_warnx (const char *msg, ...); + +void log_debug (const char *msg, ...); + +void log_info (const char *msg, ...); + +void log_vmessage (int level, int erno, const char *msg, va_list va); + +#endif /*LOG_H_*/ diff --git a/common/server-mainloop.c b/common/server-mainloop.c index d569d61..c6d2018 100644 --- a/common/server-mainloop.c +++ b/common/server-mainloop.c @@ -138,7 +138,7 @@ add_timer(int ms, int oneshot, server_timer_callback callback, void* arg) struct timeval interval; timer_callback* cb; - ASSERT(ms > 0); + ASSERT (ms || oneshot); ASSERT(callback != NULL); interval.tv_sec = ms / 1000; diff --git a/common/snmp-engine.c b/common/snmp-engine.c new file mode 100644 index 0000000..35fae26 --- /dev/null +++ b/common/snmp-engine.c @@ -0,0 +1,1011 @@ +/* + * Copyright (c) 2008, Stefan Walter + * 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 + * Stef Walter + * + */ + +#include "usuals.h" + +#include "async-resolver.h" +#include "hash.h" +#include "log.h" +#include "server-mainloop.h" +#include "snmp-engine.h" +#include "sock-any.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +struct host; +struct request; + +typedef uint64_t mstime; + +/* ------------------------------------------------------------------------------ + * HOSTS + */ + +struct host { + /* The hash key is hostname:options:community */ + char key[128]; + + char *hostname; + char *community; + int version; + + mstime interval; + + /* Host resolving and book keeping */ + struct sockaddr_any address; + mstime resolve_interval; + mstime last_resolve_try; + mstime last_resolved; + int is_resolved; + int is_resolving; + int must_resolve; + + /* Requests that are queued of this host */ + struct request *prepared; + + /* Next in list of hosts */ + struct host *next; +}; + +/* All hosts we've allocated */ +static struct host *host_list = NULL; + +/* Hosts hashed by the host:version:community string */ +static hsh_t *host_by_key = NULL; + +static void +resolve_cb (int ecode, struct addrinfo* ai, void* arg) +{ + struct host *host = (struct host*)arg; + host->is_resolving = 0; + + if (ecode) { + log_warnx ("couldn't resolve host name: %s: %s", + host->hostname, gai_strerror (ecode)); + return; + } + + /* A successful resolve */ + memcpy (&SANY_ADDR (host->address), ai->ai_addr, ai->ai_addrlen); + SANY_LEN (host->address) = ai->ai_addrlen; + host->last_resolved = server_get_time (); + host->is_resolved = 1; + + log_debug ("resolved host: %s", host->hostname); +} + +static void +host_resolve (struct host *host, mstime when) +{ + struct addrinfo hints; + + if (host->is_resolving) + return; + + memset (&hints, 0, sizeof (hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + /* Automatically strips port number */ + log_debug ("resolving host: %s", host->hostname); + host->last_resolve_try = when; + host->is_resolving = 0; + async_resolver_queue (host->hostname, "161", &hints, resolve_cb, host); +} + +static int +host_resolve_timer (mstime when, void* arg) +{ + struct host* h; + + /* Go through hosts and see which ones need resolving */ + for (h = host_list; h; h = h->next) { + + /* No need to resolve? */ + if (!h->must_resolve) + continue; + + ASSERT (h->resolve_interval); + + if (when - h->resolve_interval > h->last_resolve_try) + host_resolve (h, when); + + /* When the last 3 resolves have failed, set to unresolved */ + if (h->is_resolved && when - (h->resolve_interval * 3) > h->last_resolved) { + log_debug ("host address expired, and was not resolved: %s", h->hostname); + h->is_resolved = 0; + } + } + + return 1; +} + +static void +host_update_interval (struct host *host, mstime interval) +{ + mstime resint; + + if (!host->must_resolve) + return; + + /* When less than three minutes, resolve once per minute */ + if (interval <= 180000) + resint = 60000; + + /* When between 3 and 10 minutes resolve once per cycle */ + else if(interval <= 600000) + resint = interval; + + /* Otherwise resolve thrice per cycle */ + else + resint = interval / 3; + + /* The lowest interval (since hosts can be shared by pollers) wins */ + if (!host->resolve_interval || host->resolve_interval > resint) { + host->resolve_interval = resint; + log_debug ("will resolve host '%s' every %d seconds", host->hostname, resint / 1000); + } +} + +static struct host* +host_instance (const char *hostname, const char *community, int version, mstime interval) +{ + struct host *host; + char key[128]; + int r, initialize; + + ASSERT (hostname); + initialize = 0; + + /* + * Build a lookup key. We can only combine requests for the same + * host when the version and community match. + */ + community = community ? community : "public"; + snprintf (key, sizeof(key), "%s:%d:%s", hostname, version, community); + key[sizeof(key) - 1] = 0; + + /* See if we can find an associated host */ + host = hsh_get (host_by_key, key, -1); + if (!host) { + + host = calloc (1, sizeof (struct host)); + if (!host) { + log_errorx ("out of memory"); + return NULL; + } + + /* Try and resolve the DNS name */ + r = sock_any_pton (hostname, &host->address, SANY_OPT_DEFPORT(161) | SANY_OPT_DEFLOCAL | + SANY_OPT_NORESOLV); + if (r == -1) { + log_warn ("couldn't parse host address (ignoring): %s", hostname); + free (host); + return NULL; + } + + /* And into the hash table */ + memcpy (&host->key, key, sizeof (host->key)); + if (!hsh_set (host_by_key, host->key, -1, host)) { + log_errorx ("out of memory"); + free (host); + return NULL; + } + + /* And add it to the list */ + host->next = host_list; + host_list = host; + + /* + * If we got back SANY_AF_DNS, then it needs resolving. The actual + * interval and stuff are worked out in once all the hosts, polls etc... + * have been parsed. + */ + host->must_resolve = (r == SANY_AF_DNS); + host->is_resolved = (r != SANY_AF_DNS); + + host->version = version; + host->hostname = strdup (hostname); + host->community = strdup (community); + host->resolve_interval = 0; + host->last_resolved = 0; + host->last_resolve_try = 0; + + /* Start the resolving process */ + if (!host->is_resolved) + host_resolve (host, server_get_time ()); + } + + /* Update the host's resolve interval based on the poll interval requested */ + host_update_interval (host, interval); + + return host; +} + +static void +host_initialize (void) +{ + /* Initialize stuff if necessary */ + host_by_key = hsh_create (); + if (!host_by_key) + err (1, "out of memory"); + + /* resolve timer goes once per second */ + if (server_timer (1000, host_resolve_timer, NULL) == -1) + err (1, "couldn't setup resolve timer"); +} + +static void +host_cleanup (void) +{ + struct host *next, *host; + + if (host_by_key) + hsh_free (host_by_key); + host_by_key = NULL; + + for (host = host_list; host; host = next) { + next = host->next; + if (host->hostname) + free (host->hostname); + if (host->community) + free (host->community); + free (host); + } + + host_list = NULL; +} + +/* ------------------------------------------------------------------------------ + * ASYNC REQUEST PROCESSING + */ + +struct request +{ + /* The SNMP request identifier */ + uint id; + + mstime next_send; /* Time of the next packet send */ + mstime last_sent; /* Time last sent */ + mstime retry_interval; /* How long between retries */ + mstime when_timeout; /* When this request times out */ + uint num_sent; /* How many times we've sent */ + + struct host *host; /* Host associated with this request */ + + /* One callback entry for each binding */ + struct { + snmp_response func; + void *arg; + } callbacks[SNMP_MAX_BINDINGS]; + + /* The actual request data */ + struct snmp_pdu pdu; +}; + +/* The number of SNMP packet retries */ +static int snmp_retries = 3; + +/* The last request id */ +static uint snmp_request_id = 100000; + +/* The SNMP socket we're communicating on */ +static int snmp_socket = -1; + +/* Since we only deal with one packet at a time, global buffer */ +static unsigned char snmp_buffer[0x1000]; + +/* Hash table of all requests being processed */ +static hsh_t *snmp_processing = NULL; + +/* Hash table of all requests being prepared */ +static hsh_t *snmp_preparing = NULL; + +/* A flush of prepared packets is pending */ +static int snmp_flush_pending = 0; + +static void +request_release (struct request *req) +{ + /* It should no longer be referred to any of these places */ + ASSERT (!hsh_get (snmp_preparing, &req->id, sizeof (req->id))); + ASSERT (!hsh_get (snmp_processing, &req->id, sizeof (req->id))); + + snmp_pdu_clear (&req->pdu); + free (req); +} + +static void +request_send (struct request* req, mstime when) +{ + struct asn_buf b; + ssize_t ret; + + ASSERT (snmp_socket != -1); + + /* Update our bookkeeping */ + req->num_sent++; + if (req->num_sent <= snmp_retries) + req->next_send = when + req->retry_interval; + else + req->next_send = 0; + req->last_sent = when; + + if (!req->host->is_resolved) { + if (req->num_sent <= 1) + log_debug ("skipping snmp request: host not resolved: %s", + req->host->hostname); + return; + } + + b.asn_ptr = snmp_buffer; + b.asn_len = sizeof (snmp_buffer); + + if (snmp_pdu_encode (&req->pdu, &b)) { + log_error("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) + log_error ("couldn't send snmp packet to: %s", req->host->hostname); + else + log_debug ("sent request #%d to: %s", req->id, req->host->hostname); + } +} + +static void +request_failure (struct request *req, int code) +{ + void *val; + int j; + + ASSERT (req); + ASSERT (code != 0); + + log_debug ("failed request #%d to '%s' with code %d", req->id, req->host->hostname, code); + + /* For each request SNMP value... */ + for (j = 0; j < req->pdu.nbindings; ++j) { + /* ... let callback know */ + if (req->callbacks[j].func) + (req->callbacks[j].func) (req->id, code, NULL, req->callbacks[j].arg); + } + + /* Remove from the processing list */ + val = hsh_rem (snmp_processing, &req->id, sizeof (req->id)); + ASSERT (val == req); + + /* And free the request */ + request_release (req); +} + +static void +request_get_dispatch (struct request* req, struct snmp_pdu* pdu) +{ + struct snmp_value* pvalue; + struct snmp_value* rvalue; + int i, j, last, processed; + void *val; + + ASSERT (req); + ASSERT (pdu); + ASSERT (req->id == pdu->request_id); + ASSERT (pdu->error_status == SNMP_ERR_NOERROR); + ASSERT (req->pdu.type == SNMP_PDU_GET); + + /* + * For SNMP GET requests we check that the values that came back + * were in fact for the same values we requested, and fix any + * ordering issues etc. + */ + for (j = 0; j < req->pdu.nbindings; ++j) { + + processed = 0; + rvalue = &(req->pdu.bindings[j]); + + /* ... dig out matching value from response */ + for (i = 0; i < pdu->nbindings; ++i) { + pvalue = &(pdu->bindings[i]); + + if (asn_compare_oid (&(rvalue->var), &(pvalue->var)) != 0) + continue; + + if (req->callbacks[j].func) + (req->callbacks[j].func) (req->id, SNMP_ERR_NOERROR, + pvalue, req->callbacks[j].arg); + + processed = 1; + break; + } + + /* If this one was processed, remove from request */ + if (processed) { + last = --req->pdu.nbindings; + + ASSERT (last >= 0); + if (last) { + memcpy (&req->callbacks[j], &req->callbacks[last], sizeof (req->callbacks[j])); + memcpy (&req->pdu.bindings[j], &req->pdu.bindings[last], sizeof (req->pdu.bindings[j])); + } + memset (&req->callbacks[last], 0, sizeof (req->callbacks[last])); + memset (&req->pdu.bindings[last], 0, sizeof (req->pdu.bindings[last])); + + /* Process this index again, since we have a new request here */ + --j; + } + } + + /* All done? then remove request */ + if (req->pdu.nbindings == 0) { + + log_debug ("request #%d is complete", req->id); + + val = hsh_rem (snmp_processing, &req->id, sizeof (req->id)); + ASSERT (val == req); + request_release (req); + } +} + +static void +request_other_dispatch (struct request* req, struct snmp_pdu* pdu) +{ + void *val; + + ASSERT (req); + ASSERT (pdu); + ASSERT (req->id == pdu->request_id); + ASSERT (pdu->error_status == SNMP_ERR_NOERROR); + ASSERT (req->pdu.type != SNMP_PDU_GET); + + /* + * For requests other than GET we just use the first value + * that was sent. See below where we limit to one binding + * per SNMP request when other than GET. + */ + + if (pdu->nbindings == 0) { + log_warn ("received response from the server without any values"); + return; + } + + if (pdu->nbindings > 1) + log_warn ("received response from the server with extra values"); + + /* Shouldn't have sent more than one binding */ + ASSERT (req->pdu.nbindings == 1); + + if (req->callbacks[0].func) + (req->callbacks[0].func) (req->id, SNMP_ERR_NOERROR, + &(pdu->bindings[0]), req->callbacks[0].arg); + + log_debug ("request #%d is complete", req->id); + + val = hsh_rem (snmp_processing, &req->id, sizeof (req->id)); + ASSERT (val == req); + request_release (req); +} + +static void +request_response (int fd, int type, void* arg) +{ + char hostname[MAXPATHLEN]; + struct sockaddr_any from; + struct snmp_pdu pdu; + struct asn_buf b; + struct request* req; + const char* msg; + int len, ret; + int ip, id; + + 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) + log_error ("error receiving snmp packet from network"); + return; + } + + if (sock_any_ntop (&from, hostname, MAXPATHLEN, 0) == -1) + strcpy(hostname, "[UNKNOWN]"); + + /* 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) { + log_warnx ("invalid snmp packet received from: %s", hostname); + return; + } + + /* It needs to match something we're waiting for */ + id = pdu.request_id; + req = hsh_get (snmp_processing, &id, sizeof (id)); + if(!req) { + log_debug ("received extra or delayed packet from: %s", hostname); + return; + } + + if(pdu.version != req->pdu.version) + log_warnx ("wrong version snmp packet from: %s", hostname); + + + /* Log any errors */ + if(pdu.error_status == SNMP_ERR_NOERROR) { + log_debug ("response to request #%d from: %s", req->id, hostname); + + if (req->pdu.type == SNMP_PDU_GET) + request_get_dispatch (req, &pdu); + else + request_other_dispatch (req, &pdu); + + } else { + msg = snmp_get_errmsg (pdu.error_status); + if(msg) + log_debug ("failure for request #%d from: %s: %s", req->id, hostname, msg); + else + log_debug ("failure for request #%d from: %s: %d", req->id, hostname, + pdu.error_status); + request_failure (req, pdu.error_status); + } + +} + +static void +request_process_all (mstime when) +{ + struct request *req; + hsh_index_t *i; + + /* Go through all processing packets */ + for (i = hsh_first (snmp_processing); i; ) { + + req = hsh_this (i, NULL, NULL); + ASSERT (req); + + /* Move to the next, as we may delete below */ + i = hsh_next (i); + + if (when >= req->when_timeout) { + request_failure (req, -1); + continue; + } + + if (req->next_send && when >= req->next_send) + request_send (req, when); + } +} + +static int +request_resend_timer (mstime when, void* arg) +{ + request_process_all (when); + return 1; +} + +static void +request_flush (struct request *req, mstime when) +{ + void *val; + + ASSERT (req->host->prepared == req); + + val = hsh_rem (snmp_preparing, &req->id, sizeof (req->id)); + ASSERT (val == req); + + /* Don't let us add more onto this request via the host */ + ASSERT (req->host->prepared == req); + req->host->prepared = NULL; + + /* Mark this packet to be sent now */ + req->next_send = when; + + if (!hsh_set (snmp_processing, &req->id, sizeof (req->id), req)) { + log_errorx ("out of memory, discarding packets"); + request_release (req); + } +} + +static void +request_flush_all (mstime when) +{ + struct request *req; + hsh_index_t *i; + + /* Transfer everything to the processing table */ + for (i = hsh_first (snmp_preparing); i; ) { + req = hsh_this (i, NULL, NULL); + + /* Do this here, because below removes from table */ + i = hsh_next (i); + + request_flush (req, when); + } + + /* Clear the preparing table */ + hsh_clear (snmp_preparing); + + /* Process all packets in processing */ + request_process_all (when); +} + + + +static int +request_flush_cb (mstime when, void *arg) +{ + snmp_flush_pending = 0; + request_flush_all (when); + return 0; +} + +static struct request* +request_prep_instance (struct host *host, mstime interval, mstime timeout, int reqtype) +{ + struct request *req; + + /* See if we have one we can piggy back onto */ + req = host->prepared; + if (req) { + ASSERT (hsh_get (snmp_preparing, &req->id, sizeof (req->id))); + + /* We have one we can piggy back another request onto */ + if (req->pdu.nbindings < SNMP_MAX_BINDINGS && req->pdu.type == reqtype) + return req; + + /* It's too full, so send it off */ + request_flush (req, server_get_time ()); + req = NULL; + } + + ASSERT (host->prepared == NULL); + + /* Create a new request */ + req = calloc (1, sizeof (struct request)); + if (!req) { + log_error ("out of memory"); + return NULL; + } + + /* Assign the unique id */ + req->id = snmp_request_id++; + + /* Mark it down as something we want to prepare */ + if (!hsh_set (snmp_preparing, &req->id, sizeof (req->id), req)) { + log_error ("out of memory"); + free (req); + return NULL; + } + + /* Setup the packet */ + strlcpy (req->pdu.community, host->community, sizeof (req->pdu.community)); + req->pdu.request_id = req->id; + req->pdu.version = host->version; + req->pdu.type = reqtype; + 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->retry_interval = (interval <= 2000) ? 200L : 600L; + + /* Timeout is for the last packet sent, not first */ + req->when_timeout = server_get_time () + (req->retry_interval * ((mstime)snmp_retries)) + timeout; + req->num_sent = 0; + + /* Add it to the host */ + req->host = host; + ASSERT (host->prepared == NULL); + host->prepared = req; + + log_debug ("preparing request #%d for: %s@%s", req->id, + req->host->community, req->host->hostname); + + return req; +} + +int +snmp_engine_request (const char *hostname, const char *community, int version, + mstime interval, mstime timeout, int reqtype, + struct asn_oid *oid, snmp_response func, void *arg) +{ + struct host *host; + struct request *req; + + /* Lookup host for request */ + host = host_instance (hostname, community, version, interval); + if (!host) + return 0; + + /* Get a request with space or a new request for that host */ + req = request_prep_instance (host, interval, timeout, reqtype); + if (!req) + return 0; + + ASSERT (req->pdu.nbindings < SNMP_MAX_BINDINGS); + + /* Add the oid to that request */ + req->pdu.bindings[req->pdu.nbindings].var = *oid; + req->pdu.bindings[req->pdu.nbindings].syntax = SNMP_SYNTAX_NULL; + req->callbacks[req->pdu.nbindings].func = func; + req->callbacks[req->pdu.nbindings].arg = arg; + req->pdu.nbindings++; + + /* All other than GET, only get one binding */ + if (reqtype != SNMP_PDU_GET) { + ASSERT (req->pdu.nbindings == 1); + request_flush (req, server_get_time ()); + } + + if (!snmp_flush_pending) { + server_oneshot (0, request_flush_cb, NULL); + snmp_flush_pending = 1; + } + + return req->id; +} + +void +snmp_engine_cancel (int reqid) +{ + struct request *req; + + /* Is it being processed? */ + req = hsh_rem (snmp_processing, &reqid, sizeof (reqid)); + if (req) { + log_debug ("cancelling request #%d during processing", reqid); + request_release (req); + return; + } + + /* Is it being prepared? */ + req = hsh_rem (snmp_preparing, &reqid, sizeof (reqid)); + if (req) { + + /* Remove it from the host in question */ + ASSERT (req->host->prepared == req); + req->host->prepared = NULL; + + log_debug ("cancelling request #%d during prep", reqid); + request_release (req); + return; + } +} + +void +snmp_engine_flush (void) +{ + request_flush_all (server_get_time ()); +} + +/* ------------------------------------------------------------------------------- + * SYNC REQUESTS + */ + +struct sync_data { + int valid; + int code; + int id; + struct snmp_value *dest; +}; + +static void +sync_response (int req, int code, struct snmp_value *value, void *data) +{ + struct sync_data *sync = data; + + ASSERT (req == sync->id); + + sync->valid = 1; + sync->code = code; + if (value) + snmp_value_copy (sync->dest, value); + + server_stop (); +} + +int +snmp_engine_sync (const char* host, const char* community, int version, + uint64_t interval, uint64_t timeout, int reqtype, + struct snmp_value *value) +{ + struct sync_data sync; + + /* Can't run a sync request with the server running */ + ASSERT (server_stopped()); + + sync.valid = 0; + sync.code = 0; + sync.dest = value; + + sync.id = snmp_engine_request (host, community, version, interval, timeout, + reqtype, &value->var, sync_response, &sync); + + if (!sync.id) + return -1; + + snmp_engine_flush (); + server_run (); + + ASSERT (sync.valid); + return sync.code; +} + +/* ----------------------------------------------------------------------------- + * INIT + */ + +void +snmp_engine_init (int retries) +{ + struct sockaddr_in addr; + + snmp_retries = retries; + + snmp_processing = hsh_create (); + if (!snmp_processing) + err (1, "out of memory"); + + snmp_preparing = hsh_create (); + if (!snmp_preparing) + err (1, "out of memory"); + + 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, request_response, NULL) == -1) + err (1, "couldn't listen on socket"); + + /* We fire off the resend timer every 1/5 second */ + if (server_timer (200, request_resend_timer, NULL) == -1) + err(1, "couldn't setup timer"); + + host_initialize (); +} + +void +snmp_engine_stop (void) +{ + if (snmp_socket != -1) { + server_unwatch (snmp_socket); + close (snmp_socket); + snmp_socket = -1; + } + + host_cleanup (); +} + +int +snmp_engine_match (const struct snmp_value *value, const char *text) +{ + char *end; + + ASSERT (value); + ASSERT (text); + + switch (value->syntax) { + + /* Empty string */ + case SNMP_SYNTAX_NULL: + case SNMP_SYNTAX_NOSUCHOBJECT: + case SNMP_SYNTAX_NOSUCHINSTANCE: + case SNMP_SYNTAX_ENDOFMIBVIEW: + return *text == '\0'; + + /* Integer value */ + case SNMP_SYNTAX_INTEGER: + { + int num = strtoll (text, &end, 0); + if (*end != '\0') + return 0; + return num == value->v.integer; + } + + /* String of bytes */ + case SNMP_SYNTAX_OCTETSTRING: + { + int len = strlen (text); + if (value->v.octetstring.len != len) + return 0; + return memcmp (value->v.octetstring.octets, text, len) == 0; + } + + + case SNMP_SYNTAX_OID: + { + struct asn_oid oid; + if (mib_parse (text, &oid) < 0) + return 0; + return asn_compare_oid (&oid, &value->v.oid) == 0; + } + + case SNMP_SYNTAX_IPADDRESS: + { + struct in_addr addr; + if (!inet_aton (text, &addr)) + return 0; + return memcmp (&addr, value->v.ipaddress, 4) == 0; + } + + case SNMP_SYNTAX_COUNTER: + case SNMP_SYNTAX_GAUGE: + case SNMP_SYNTAX_TIMETICKS: + { + uint64_t sub = strtoull (text, &end, 0); + if (*end != '\0' || sub > 0xffffffff) + return 0; + return sub == value->v.uint32; + } + + case SNMP_SYNTAX_COUNTER64: + { + uint64_t sub = strtoull (text, &end, 0); + if (*end != '\0' || sub > 0xffffffff) + return 0; + return sub == value->v.counter64; + } + + default: + return 0; + }; +} diff --git a/common/snmp-engine.h b/common/snmp-engine.h new file mode 100644 index 0000000..36b2a3d --- /dev/null +++ b/common/snmp-engine.h @@ -0,0 +1,27 @@ +#ifndef SNMPENGINE_H_ +#define SNMPENGINE_H_ + +#include +#include + +typedef void (*snmp_response) (int request, int code, struct snmp_value *value, void *data); + +void snmp_engine_init (int retries); + +int snmp_engine_request (const char* host, const char* community, int version, + uint64_t interval, uint64_t timeout, int reqtype, + struct asn_oid *oid, snmp_response func, void *data); + +void snmp_engine_cancel (int reqid); + +void snmp_engine_flush (void); + +int snmp_engine_sync (const char* host, const char* community, int version, + uint64_t interval, uint64_t timeout, int reqtype, + struct snmp_value *value); + +void snmp_engine_stop (void); + +int snmp_engine_match (const struct snmp_value *value, const char *text); + +#endif /*SNMPENGINE_H_*/ -- cgit v1.2.3