/* Portions of this file are subject to the following copyright(s). See * the Net-SNMP's COPYING file for more details and other copyrights * that may apply: */ /* * Portions of this file are copyrighted by: * Copyright (C) 2007 Apple, Inc. All rights reserved. * Use is subject to license terms specified in the COPYING file * distributed with the Net-SNMP package. * * Portions of this file are copyrighted by: * Copyright (c) 2016 VMware, Inc. All rights reserved. * Use is subject to license terms specified in the COPYING file * distributed with the Net-SNMP package. */ #include #include #if HAVE_STRING_H #include #else #include #endif #include #include #include netsnmp_feature_child_of(cache_handler, mib_helpers) netsnmp_feature_child_of(cache_find_by_oid, cache_handler) netsnmp_feature_child_of(cache_get_head, cache_handler) static netsnmp_cache *cache_head = NULL; static int cache_outstanding_valid = 0; static int _cache_load( netsnmp_cache *cache ); #define CACHE_RELEASE_FREQUENCY 60 /* Check for expired caches every 60s */ void release_cached_resources(unsigned int regNo, void *clientargs); /** @defgroup cache_handler cache_handler * Maintains a cache of data for use by lower level handlers. * @ingroup utilities * This helper checks to see whether the data has been loaded "recently" * (according to the timeout for that particular cache) and calls the * registered "load_cache" routine if necessary. * The lower handlers can then work with this local cached data. * * A timeout value of -1 will cause netsnmp_cache_check_expired() to * always return true, and thus the cache will be reloaded for every * request. * * To minimze resource use by the agent, a periodic callback checks for * expired caches, and will call the free_cache function for any expired * cache. * * The load_cache routine should return a negative number if the cache * was not successfully loaded. 0 or any positive number indicates successs. * * * Several flags can be set to affect the operations on the cache. * * If NETSNMP_CACHE_DONT_INVALIDATE_ON_SET is set, the free_cache method * will not be called after a set request has processed. It is assumed that * the lower mib handler using the cache has maintained cache consistency. * * If NETSNMP_CACHE_DONT_FREE_BEFORE_LOAD is set, the free_cache method * will not be called before the load_cache method is called. It is assumed * that the load_cache routine will properly deal with being called with a * valid cache. * * If NETSNMP_CACHE_DONT_FREE_EXPIRED is set, the free_cache method will * not be called with the cache expires. The expired flag will be set, but * the valid flag will not be cleared. It is assumed that the load_cache * routine will properly deal with being called with a valid cache. * * If NETSNMP_CACHE_PRELOAD is set when a the cache handler is created, * the cache load routine will be called immediately. * * If NETSNMP_CACHE_DONT_AUTO_RELEASE is set, the periodic callback that * checks for expired caches will skip the cache. The cache will only be * checked for expiration when a request triggers the cache handler. This * is useful if the cache has it's own periodic callback to keep the cache * fresh. * * If NETSNMP_CACHE_AUTO_RELOAD is set, a timer will be set up to reload * the cache when it expires. This is useful for keeping the cache fresh, * even in the absence of incoming snmp requests. * * If NETSNMP_CACHE_RESET_TIMER_ON_USE is set, the expiry timer will be * reset on each cache access. In practice the 'timeout' becomes a timer * which triggers when the cache is no longer needed. This is useful * if the cache is automatically kept synchronized: e.g. by receiving * change notifications from Netlink, inotify or similar. This should * not be used if cache is not synchronized automatically as it would * result in stale cache information when if polling happens too fast. * * * Here are some suggestions for some common situations. * * Cached File: * If your table is based on a file that may periodically change, * you can test the modification date to see if the file has * changed since the last cache load. To get the cache helper to call * the load function for every request, set the timeout to -1, which * will cause the cache to always report that it is expired. This means * that you will want to prevent the agent from flushing the cache when * it has expired, and you will have to flush it manually if you * detect that the file has changed. To accomplish this, set the * following flags: * * NETSNMP_CACHE_DONT_FREE_EXPIRED * NETSNMP_CACHE_DONT_AUTO_RELEASE * * * Constant (periodic) reload: * If you want the cache kept up to date regularly, even if no requests * for the table are received, you can have your cache load routine * called periodically. This is very useful if you need to monitor the * data for changes (eg a LastChanged object). You will need to * prevent the agent from flushing the cache when it expires. Set the * cache timeout to the frequency, in seconds, that you wish to * reload your cache, and set the following flags: * * NETSNMP_CACHE_DONT_FREE_EXPIRED * NETSNMP_CACHE_DONT_AUTO_RELEASE * NETSNMP_CACHE_AUTO_RELOAD * * Dynamically updated, unloaded after timeout: * If the cache is kept up to date dynamically by listening for * change notifications somehow, but it should not be in memory * if it's not needed. Set the following flag: * * NETSNMP_CACHE_RESET_TIMER_ON_USE * * @{ */ static void _cache_free( netsnmp_cache *cache ); #ifndef NETSNMP_FEATURE_REMOVE_CACHE_GET_HEAD /** get cache head * @internal * unadvertised function to get cache head. You really should not * do this, since the internal storage mechanism might change. */ netsnmp_cache * netsnmp_cache_get_head(void) { return cache_head; } #endif /* NETSNMP_FEATURE_REMOVE_CACHE_GET_HEAD */ #ifndef NETSNMP_FEATURE_REMOVE_CACHE_FIND_BY_OID /** find existing cache */ netsnmp_cache * netsnmp_cache_find_by_oid(const oid * rootoid, int rootoid_len) { netsnmp_cache *cache; for (cache = cache_head; cache; cache = cache->next) { if (0 == netsnmp_oid_equals(cache->rootoid, cache->rootoid_len, rootoid, rootoid_len)) return cache; } return NULL; } #endif /* NETSNMP_FEATURE_REMOVE_CACHE_FIND_BY_OID */ /** returns a cache */ netsnmp_cache * netsnmp_cache_create(int timeout, NetsnmpCacheLoad * load_hook, NetsnmpCacheFree * free_hook, const oid * rootoid, int rootoid_len) { netsnmp_cache *cache = NULL; cache = SNMP_MALLOC_TYPEDEF(netsnmp_cache); if (NULL == cache) { snmp_log(LOG_ERR,"malloc error in netsnmp_cache_create\n"); return NULL; } cache->timeout = timeout; cache->load_cache = load_hook; cache->free_cache = free_hook; cache->enabled = 1; if(0 == cache->timeout) cache->timeout = netsnmp_ds_get_int(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_CACHE_TIMEOUT); /* * Add the registered OID information, and tack * this onto the list for cache SNMP management * * Note that this list is not ordered. * table_iterator rules again! */ if (rootoid) { cache->rootoid = snmp_duplicate_objid(rootoid, rootoid_len); cache->rootoid_len = rootoid_len; cache->next = cache_head; if (cache_head) cache_head->prev = cache; cache_head = cache; } return cache; } static netsnmp_cache * netsnmp_cache_ref(netsnmp_cache *cache) { cache->refcnt++; return cache; } static void netsnmp_cache_deref(netsnmp_cache *cache) { if (--cache->refcnt == 0) { netsnmp_cache_remove(cache); netsnmp_cache_free(cache); } } /** frees a cache */ int netsnmp_cache_free(netsnmp_cache *cache) { netsnmp_cache *pos; if (NULL == cache) return SNMPERR_SUCCESS; for (pos = cache_head; pos; pos = pos->next) { if (pos == cache) { size_t out_len = 0; size_t buf_len = 0; char *buf = NULL; sprint_realloc_objid((u_char **) &buf, &buf_len, &out_len, 1, pos->rootoid, pos->rootoid_len); snmp_log(LOG_WARNING, "not freeing cache with root OID %s (still in list)\n", buf); free(buf); return SNMP_ERR_GENERR; } } if(0 != cache->timer_id) netsnmp_cache_timer_stop(cache); if (cache->valid) _cache_free(cache); if (cache->timestampM) free(cache->timestampM); if (cache->rootoid) free(cache->rootoid); free(cache); return SNMPERR_SUCCESS; } /** removes a cache */ int netsnmp_cache_remove(netsnmp_cache *cache) { netsnmp_cache *cur,*prev; if (!cache || !cache_head) return -1; if (cache == cache_head) { cache_head = cache_head->next; if (cache_head) cache_head->prev = NULL; return 0; } prev = cache_head; cur = cache_head->next; for (; cur; prev = cur, cur = cur->next) { if (cache == cur) { prev->next = cur->next; if (cur->next) cur->next->prev = cur->prev; return 0; } } return -1; } /** callback function to call cache load function */ static void _timer_reload(unsigned int regNo, void *clientargs) { netsnmp_cache *cache = (netsnmp_cache *)clientargs; DEBUGMSGT(("cache_timer:start", "loading cache %p\n", cache)); cache->expired = 1; _cache_load(cache); } /** starts the recurring cache_load callback */ unsigned int netsnmp_cache_timer_start(netsnmp_cache *cache) { if(NULL == cache) return 0; DEBUGMSGTL(( "cache_timer:start", "OID: ")); DEBUGMSGOID(("cache_timer:start", cache->rootoid, cache->rootoid_len)); DEBUGMSG(( "cache_timer:start", "\n")); if(0 != cache->timer_id) { snmp_log(LOG_WARNING, "cache has existing timer id.\n"); return cache->timer_id; } if(! (cache->flags & NETSNMP_CACHE_AUTO_RELOAD)) { snmp_log(LOG_ERR, "cache_timer_start called but auto_reload not set.\n"); return 0; } cache->timer_id = snmp_alarm_register(cache->timeout, SA_REPEAT, _timer_reload, cache); if(0 == cache->timer_id) { snmp_log(LOG_ERR,"could not register alarm\n"); return 0; } cache->flags &= ~NETSNMP_CACHE_AUTO_RELOAD; DEBUGMSGT(("cache_timer:start", "starting timer %lu for cache %p\n", cache->timer_id, cache)); return cache->timer_id; } /** stops the recurring cache_load callback */ void netsnmp_cache_timer_stop(netsnmp_cache *cache) { if(NULL == cache) return; if(0 == cache->timer_id) { snmp_log(LOG_WARNING, "cache has no timer id.\n"); return; } DEBUGMSGT(("cache_timer:stop", "stopping timer %lu for cache %p\n", cache->timer_id, cache)); snmp_alarm_unregister(cache->timer_id); cache->flags |= NETSNMP_CACHE_AUTO_RELOAD; } /** returns a cache handler that can be injected into a given handler chain. */ netsnmp_mib_handler * netsnmp_cache_handler_get(netsnmp_cache* cache) { netsnmp_mib_handler *ret = NULL; ret = netsnmp_create_handler("cache_handler", netsnmp_cache_helper_handler); if (ret) { ret->flags |= MIB_HANDLER_AUTO_NEXT; ret->myvoid = (void *) cache; if(NULL != cache) { if ((cache->flags & NETSNMP_CACHE_PRELOAD) && ! cache->valid) { /* * load cache, ignore rc * (failed load doesn't affect registration) */ (void)_cache_load(cache); } if (cache->flags & NETSNMP_CACHE_AUTO_RELOAD) netsnmp_cache_timer_start(cache); } } return ret; } /** Makes sure that memory allocated for the cache is freed when the handler * is unregistered. */ void netsnmp_cache_handler_owns_cache(netsnmp_mib_handler *handler) { netsnmp_assert(handler->myvoid); ((netsnmp_cache *)handler->myvoid)->refcnt++; handler->data_clone = (void *(*)(void *))netsnmp_cache_ref; handler->data_free = (void(*)(void*))netsnmp_cache_deref; } /** returns a cache handler that can be injected into a given handler chain. */ netsnmp_mib_handler * netsnmp_get_cache_handler(int timeout, NetsnmpCacheLoad * load_hook, NetsnmpCacheFree * free_hook, const oid * rootoid, int rootoid_len) { netsnmp_mib_handler *ret = NULL; netsnmp_cache *cache = NULL; ret = netsnmp_cache_handler_get(NULL); if (ret) { cache = netsnmp_cache_create(timeout, load_hook, free_hook, rootoid, rootoid_len); ret->myvoid = (void *) cache; netsnmp_cache_handler_owns_cache(ret); } return ret; } #if !defined(NETSNMP_FEATURE_REMOVE_NETSNMP_CACHE_HANDLER_REGISTER) || !defined(NETSNMP_FEATURE_REMOVE_NETSNMP_REGISTER_CACHE_HANDLER) static int _cache_handler_register(netsnmp_handler_registration * reginfo, netsnmp_mib_handler *handler) { /** success path */ if (reginfo && handler && (netsnmp_inject_handler(reginfo, handler) == SNMPERR_SUCCESS)) return netsnmp_register_handler(reginfo); /** error path */ snmp_log(LOG_ERR, "could not register cache handler\n"); if (handler) netsnmp_handler_free(handler); netsnmp_handler_registration_free(reginfo); return MIB_REGISTRATION_FAILED; } #endif /** functionally the same as calling netsnmp_register_handler() but also * injects a cache handler at the same time for you. */ netsnmp_feature_child_of(netsnmp_cache_handler_register,netsnmp_unused) #ifndef NETSNMP_FEATURE_REMOVE_NETSNMP_CACHE_HANDLER_REGISTER int netsnmp_cache_handler_register(netsnmp_handler_registration * reginfo, netsnmp_cache* cache) { if ((NULL == reginfo) || (NULL == cache)) { snmp_log(LOG_ERR, "bad param in netsnmp_cache_handler_register\n"); netsnmp_handler_registration_free(reginfo); return MIB_REGISTRATION_FAILED; } return _cache_handler_register(reginfo, netsnmp_cache_handler_get(cache)); } #endif /* NETSNMP_FEATURE_REMOVE_NETSNMP_CACHE_HANDLER_REGISTER */ /** functionally the same as calling netsnmp_register_handler() but also * injects a cache handler at the same time for you. */ netsnmp_feature_child_of(netsnmp_register_cache_handler,netsnmp_unused) #ifndef NETSNMP_FEATURE_REMOVE_NETSNMP_REGISTER_CACHE_HANDLER int netsnmp_register_cache_handler(netsnmp_handler_registration * reginfo, int timeout, NetsnmpCacheLoad * load_hook, NetsnmpCacheFree * free_hook) { netsnmp_mib_handler *handler; if (NULL == reginfo) { snmp_log(LOG_ERR, "bad param in netsnmp_cache_handler_register\n"); netsnmp_handler_registration_free(reginfo); return MIB_REGISTRATION_FAILED; } handler = netsnmp_get_cache_handler(timeout, load_hook, free_hook, reginfo->rootoid, reginfo->rootoid_len); return _cache_handler_register(reginfo, handler); } #endif /* NETSNMP_FEATURE_REMOVE_NETSNMP_REGISTER_CACHE_HANDLER */ static char * _build_cache_name(const char *name) { char *dup = (char*)malloc(strlen(name) + strlen(CACHE_NAME) + 2); if (NULL == dup) return NULL; sprintf(dup, "%s:%s", CACHE_NAME, name); return dup; } /** Insert the cache information for a given request (PDU) */ void netsnmp_cache_reqinfo_insert(netsnmp_cache* cache, netsnmp_agent_request_info * reqinfo, const char *name) { char *cache_name = _build_cache_name(name); if (NULL == netsnmp_agent_get_list_data(reqinfo, cache_name)) { DEBUGMSGTL(("verbose:helper:cache_handler", " adding '%s' to %p\n", cache_name, reqinfo)); netsnmp_agent_add_list_data(reqinfo, netsnmp_create_data_list(cache_name, cache, NULL)); } SNMP_FREE(cache_name); } /** Extract the cache information for a given request (PDU) */ netsnmp_cache * netsnmp_cache_reqinfo_extract(netsnmp_agent_request_info * reqinfo, const char *name) { netsnmp_cache *result; char *cache_name = _build_cache_name(name); result = (netsnmp_cache*)netsnmp_agent_get_list_data(reqinfo, cache_name); SNMP_FREE(cache_name); return result; } /** Extract the cache information for a given request (PDU) */ netsnmp_feature_child_of(netsnmp_extract_cache_info,netsnmp_unused) #ifndef NETSNMP_FEATURE_REMOVE_NETSNMP_EXTRACT_CACHE_INFO netsnmp_cache * netsnmp_extract_cache_info(netsnmp_agent_request_info * reqinfo) { return netsnmp_cache_reqinfo_extract(reqinfo, CACHE_NAME); } #endif /* NETSNMP_FEATURE_REMOVE_NETSNMP_EXTRACT_CACHE_INFO */ /** Check if the cache timeout has passed. Sets and return the expired flag. */ int netsnmp_cache_check_expired(netsnmp_cache *cache) { if(NULL == cache) return 0; if (cache->expired) return 1; if(!cache->valid || (NULL == cache->timestampM) || (-1 == cache->timeout)) cache->expired = 1; else cache->expired = netsnmp_ready_monotonic(cache->timestampM, 1000 * cache->timeout); return cache->expired; } /** Reload the cache if required */ int netsnmp_cache_check_and_reload(netsnmp_cache * cache) { if (!cache) { DEBUGMSGT(("helper:cache_handler", " no cache\n")); return 0; /* ?? or -1 */ } if (!cache->valid || netsnmp_cache_check_expired(cache)) return _cache_load( cache ); else { DEBUGMSGT(("helper:cache_handler", " cached (%d)\n", cache->timeout)); return 0; } } /** Is the cache valid for a given request? */ int netsnmp_cache_is_valid(netsnmp_agent_request_info * reqinfo, const char* name) { netsnmp_cache *cache = netsnmp_cache_reqinfo_extract(reqinfo, name); return (cache && cache->valid); } /** Is the cache valid for a given request? * for backwards compatability. netsnmp_cache_is_valid() is preferred. */ netsnmp_feature_child_of(netsnmp_is_cache_valid,netsnmp_unused) #ifndef NETSNMP_FEATURE_REMOVE_NETSNMP_IS_CACHE_VALID int netsnmp_is_cache_valid(netsnmp_agent_request_info * reqinfo) { return netsnmp_cache_is_valid(reqinfo, CACHE_NAME); } #endif /* NETSNMP_FEATURE_REMOVE_NETSNMP_IS_CACHE_VALID */ /** Implements the cache handler */ int netsnmp_cache_helper_handler(netsnmp_mib_handler * handler, netsnmp_handler_registration * reginfo, netsnmp_agent_request_info * reqinfo, netsnmp_request_info * requests) { char addrstr[32]; netsnmp_cache *cache = NULL; netsnmp_handler_args cache_hint; DEBUGMSGTL(("helper:cache_handler", "Got request (%d) for %s: ", reqinfo->mode, reginfo->handlerName)); DEBUGMSGOID(("helper:cache_handler", reginfo->rootoid, reginfo->rootoid_len)); DEBUGMSG(("helper:cache_handler", "\n")); netsnmp_assert(handler->flags & MIB_HANDLER_AUTO_NEXT); cache = (netsnmp_cache *) handler->myvoid; if (netsnmp_ds_get_boolean(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_NO_CACHING) || !cache || !cache->enabled || !cache->load_cache) { DEBUGMSGT(("helper:cache_handler", " caching disabled or " "cache not found, disabled or had no load method\n")); return SNMP_ERR_NOERROR; } snprintf(addrstr, sizeof(addrstr), "%p", cache); DEBUGMSGTL(("helper:cache_handler", "using cache %s: ", addrstr)); DEBUGMSGOID(("helper:cache_handler", cache->rootoid, cache->rootoid_len)); DEBUGMSG(("helper:cache_handler", "\n")); /* * Make the handler-chain parameters available to * the cache_load hook routine. */ cache_hint.handler = handler; cache_hint.reginfo = reginfo; cache_hint.reqinfo = reqinfo; cache_hint.requests = requests; cache->cache_hint = &cache_hint; switch (reqinfo->mode) { case MODE_GET: case MODE_GETNEXT: case MODE_GETBULK: #ifndef NETSNMP_NO_WRITE_SUPPORT case MODE_SET_RESERVE1: #endif /* !NETSNMP_NO_WRITE_SUPPORT */ /* * only touch cache once per pdu request, to prevent a cache * reload while a module is using cached data. * * XXX: this won't catch a request reloading the cache while * a previous (delegated) request is still using the cache. * maybe use a reference counter? */ if (netsnmp_cache_is_valid(reqinfo, addrstr)) break; /* * call the load hook, and update the cache timestamp. * If it's not already there, add to reqinfo */ netsnmp_cache_check_and_reload(cache); netsnmp_cache_reqinfo_insert(cache, reqinfo, addrstr); /** next handler called automatically - 'AUTO_NEXT' */ break; #ifndef NETSNMP_NO_WRITE_SUPPORT case MODE_SET_RESERVE2: case MODE_SET_FREE: case MODE_SET_ACTION: case MODE_SET_UNDO: netsnmp_assert(netsnmp_cache_is_valid(reqinfo, addrstr)); /** next handler called automatically - 'AUTO_NEXT' */ break; /* * A (successful) SET request wouldn't typically trigger a reload of * the cache, but might well invalidate the current contents. * Only do this on the last pass through. */ case MODE_SET_COMMIT: if (cache->valid && ! (cache->flags & NETSNMP_CACHE_DONT_INVALIDATE_ON_SET) ) { cache->free_cache(cache, cache->magic); cache->valid = 0; } /** next handler called automatically - 'AUTO_NEXT' */ break; #endif /* NETSNMP_NO_WRITE_SUPPORT */ default: snmp_log(LOG_WARNING, "cache_handler: Unrecognised mode (%d)\n", reqinfo->mode); netsnmp_request_set_error_all(requests, SNMP_ERR_GENERR); return SNMP_ERR_GENERR; } if (cache->flags & NETSNMP_CACHE_RESET_TIMER_ON_USE) netsnmp_set_monotonic_marker(&cache->timestampM); return SNMP_ERR_NOERROR; } static void _cache_free( netsnmp_cache *cache ) { if (NULL != cache->free_cache) { cache->free_cache(cache, cache->magic); cache->valid = 0; } } static int _cache_load( netsnmp_cache *cache ) { int ret = -1; /* * If we've got a valid cache, then release it before reloading */ if (cache->valid && (! (cache->flags & NETSNMP_CACHE_DONT_FREE_BEFORE_LOAD))) _cache_free(cache); if ( cache->load_cache) ret = cache->load_cache(cache, cache->magic); if (ret < 0) { DEBUGMSGT(("helper:cache_handler", " load failed (%d)\n", ret)); cache->valid = 0; return ret; } cache->valid = 1; cache->expired = 0; /* * If we didn't previously have any valid caches outstanding, * then schedule a pass of the auto-release routine. */ if ((!cache_outstanding_valid) && (! (cache->flags & NETSNMP_CACHE_DONT_FREE_EXPIRED))) { snmp_alarm_register(CACHE_RELEASE_FREQUENCY, 0, release_cached_resources, NULL); cache_outstanding_valid = 1; } netsnmp_set_monotonic_marker(&cache->timestampM); DEBUGMSGT(("helper:cache_handler", " loaded (%d)\n", cache->timeout)); return ret; } /** run regularly to automatically release cached resources. * xxx - method to prevent cache from expiring while a request * is being processed (e.g. delegated request). proposal: * set a flag, which would be cleared when request finished * (which could be acomplished by a dummy data list item in * agent req info & custom free function). */ void release_cached_resources(unsigned int regNo, void *clientargs) { netsnmp_cache *cache = NULL; cache_outstanding_valid = 0; DEBUGMSGTL(("helper:cache_handler", "running auto-release\n")); for (cache = cache_head; cache; cache = cache->next) { DEBUGMSGTL(("helper:cache_handler"," checking %p (flags 0x%x)\n", cache, cache->flags)); if (cache->valid && ! (cache->flags & NETSNMP_CACHE_DONT_AUTO_RELEASE)) { DEBUGMSGTL(("helper:cache_handler"," releasing %p\n", cache)); /* * Check to see if this cache has timed out. * If so, release the cached resources. * Otherwise, note that we still have at * least one active cache. */ if (netsnmp_cache_check_expired(cache)) { if(! (cache->flags & NETSNMP_CACHE_DONT_FREE_EXPIRED)) _cache_free(cache); } else { cache_outstanding_valid = 1; } } } /* * If there are any caches still valid & active, * then schedule another pass. */ if (cache_outstanding_valid) { snmp_alarm_register(CACHE_RELEASE_FREQUENCY, 0, release_cached_resources, NULL); } } /** @} */