SnmpQuery runtime cache (#15512)

* SnmpQuery runtime cache
Optionally cache the SnmpResponse from queries for the rest of the runtime.
It would be nice to have cache hits for these to help prevent wasting memory caching data that is only used once.
Key data must match: device, context, options, oids

* Apply fixes from StyleCI

* Add command to the cache key

* Caching in the wrong spot, cache individual commands...

* Apply fixes from StyleCI

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
This commit is contained in:
Tony Murray 2023-10-29 22:46:04 -05:00 committed by GitHub
parent 6bc8a504bb
commit 7c542d70af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 14 deletions

View File

@ -30,6 +30,7 @@ use App\Models\Eventlog;
use App\Polling\Measure\Measurement;
use DeviceCache;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Enum\Severity;
@ -82,6 +83,7 @@ class NetSnmpQuery implements SnmpQueryInterface
private array|string $options = [self::DEFAULT_FLAGS];
private Device $device;
private bool $abort = false;
private bool $cache = false;
// defaults for net-snmp https://net-snmp.sourceforge.io/docs/man/snmpcmd.html
private array $mibs = ['SNMPv2-TC', 'SNMPv2-MIB', 'IF-MIB', 'IP-MIB', 'TCP-MIB', 'UDP-MIB', 'NET-SNMP-VACM-MIB'];
@ -126,6 +128,13 @@ class NetSnmpQuery implements SnmpQueryInterface
return $this;
}
public function cache(): SnmpQueryInterface
{
$this->cache = true;
return $this;
}
/**
* Set a context for the snmp query
* This is most commonly used to fetch alternate sets of data, such as different VRFs
@ -201,6 +210,18 @@ class NetSnmpQuery implements SnmpQueryInterface
return $this;
}
/**
* Output all OIDs numerically
*/
public function numericIndex(bool $numericIndex = true): SnmpQueryInterface
{
$this->options = $numericIndex
? array_merge($this->options, ['-Ob'])
: array_diff($this->options, ['-Ob']);
return $this;
}
/**
* Hide MIB in output
*/
@ -373,7 +394,8 @@ class NetSnmpQuery implements SnmpQueryInterface
// if abort on failure is set, return after first failure
if ($this->abort && ! $response->isValid()) {
Log::debug("SNMP failed walking $oid of " . implode(',', $oids) . ' aborting.');
$oid_list = implode(',', array_map(fn ($group) => implode(',', $group), $oids));
Log::debug("SNMP failed walking $oid of $oid_list aborting.");
return $response;
}
@ -384,25 +406,30 @@ class NetSnmpQuery implements SnmpQueryInterface
private function exec(string $command, array $oids): SnmpResponse
{
$measure = Measurement::start($command);
// use runtime(array) cache if requested. The 'null' driver will simply return the value without caching
$driver = $this->cache ? 'array' : 'null';
$key = $this->cache ? $this->getCacheKey($command, $oids) : '';
$proc = new Process($this->buildCli($command, $oids));
$proc->setTimeout(Config::get('snmp.exec_timeout', 1200));
return Cache::driver($driver)->rememberForever($key, function () use ($command, $oids) {
$measure = Measurement::start($command);
$proc = new Process($this->buildCli($command, $oids));
$proc->setTimeout(Config::get('snmp.exec_timeout', 1200));
$this->logCommand($proc->getCommandLine());
$this->logCommand($proc->getCommandLine());
$proc->run();
$exitCode = $proc->getExitCode();
$output = $proc->getOutput();
$stderr = $proc->getErrorOutput();
$proc->run();
$exitCode = $proc->getExitCode();
$output = $proc->getOutput();
$stderr = $proc->getErrorOutput();
// check exit code and log possible bad auth
$this->checkExitCode($exitCode, $stderr);
$this->logOutput($output, $stderr);
// check exit code and log possible bad auth
$this->checkExitCode($exitCode, $stderr);
$this->logOutput($output, $stderr);
$measure->manager()->recordSnmp($measure->end());
$measure->manager()->recordSnmp($measure->end());
return new SnmpResponse($output, $stderr, $exitCode);
return new SnmpResponse($output, $stderr, $exitCode);
});
}
private function initCommand(string $binary, array $oids): array
@ -513,4 +540,12 @@ class NetSnmpQuery implements SnmpQueryInterface
{
return is_string($oid) ? explode(' ', $oid) : $oid;
}
private function getCacheKey(string $type, array $oids): string
{
$oids = implode(',', $oids);
$options = implode(',', $this->options);
return "$type|{$this->device->hostname}|$this->context|$oids|$options";
}
}

View File

@ -46,6 +46,11 @@ interface SnmpQueryInterface
*/
public function deviceArray(array $device): SnmpQueryInterface;
/**
* Cache the data for the rest of the runtime (or retrieve from cache if available)
*/
public function cache(): SnmpQueryInterface;
/**
* Set a context for the snmp query
* This is most commonly used to fetch alternate sets of data, such as different VRFs
@ -81,6 +86,11 @@ interface SnmpQueryInterface
*/
public function numeric(bool $numeric = true): SnmpQueryInterface;
/**
* Output indexes only as numeric
*/
public function numericIndex(bool $numericIndex = true): SnmpQueryInterface;
/**
* Hide MIB in output
*/

View File

@ -286,4 +286,9 @@ class SnmpResponse
// regular oid
return explode('.', $key);
}
public function __sleep()
{
return ['raw', 'exitCode', 'stderr'];
}
}

View File

@ -73,6 +73,11 @@ class SnmpQueryMock implements SnmpQueryInterface
return $this;
}
public function cache(): SnmpQueryInterface
{
return $this; // ignore, always cached
}
public function context(string $context): SnmpQueryInterface
{
$this->context = $context;
@ -117,6 +122,14 @@ class SnmpQueryMock implements SnmpQueryInterface
return $this;
}
public function numericIndex(bool $numericIndex = true): SnmpQueryInterface
{
// TODO: Implement numericIndex() method
Log::error('numericIndex not implemented in SnmpQueryMock');
return $this;
}
public function hideMib(): SnmpQueryInterface
{
$this->hideMib = true;