mirror of
https://github.com/librenms/librenms.git
synced 2024-10-07 16:52:45 +00:00
Previously, the code would query all the oids it received. Now it will split it up into multiple queries if too many are sent. Prevents some devices snmp service from crashing.
517 lines
16 KiB
PHP
517 lines
16 KiB
PHP
<?php
|
|
/*
|
|
* SNMP.php
|
|
*
|
|
* -Description-
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @package LibreNMS
|
|
* @link http://librenms.org
|
|
* @copyright 2021 Tony Murray
|
|
* @author Tony Murray <murraytony@gmail.com>
|
|
*/
|
|
|
|
namespace LibreNMS\Data\Source;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\Eventlog;
|
|
use App\Polling\Measure\Measurement;
|
|
use DeviceCache;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
use LibreNMS\Config;
|
|
use LibreNMS\Enum\Alert;
|
|
use LibreNMS\Util\Debug;
|
|
use LibreNMS\Util\Oid;
|
|
use LibreNMS\Util\Rewrite;
|
|
use Log;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
class NetSnmpQuery implements SnmpQueryInterface
|
|
{
|
|
private const DEFAULT_FLAGS = '-OQXUte';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $cleanup = [
|
|
'command' => [
|
|
[
|
|
'/-c\' \'[\S]+\'/',
|
|
'/-u\' \'[\S]+\'/',
|
|
'/-U\' \'[\S]+\'/',
|
|
'/-A\' \'[\S]+\'/',
|
|
'/-X\' \'[\S]+\'/',
|
|
'/-P\' \'[\S]+\'/',
|
|
'/-H\' \'[\S]+\'/',
|
|
'/(udp|udp6|tcp|tcp6):([^:]+):([\d]+)/',
|
|
], [
|
|
'-c\' \'COMMUNITY\'',
|
|
'-u\' \'USER\'',
|
|
'-U\' \'USER\'',
|
|
'-A\' \'PASSWORD\'',
|
|
'-X\' \'PASSWORD\'',
|
|
'-P\' \'PASSWORD\'',
|
|
'-H\' \'HOSTNAME\'',
|
|
'\1:HOSTNAME:\3',
|
|
],
|
|
],
|
|
'output' => [
|
|
'/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/',
|
|
'*',
|
|
],
|
|
];
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $context = '';
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
private array $mibDirs = [];
|
|
/**
|
|
* @var array|string
|
|
*/
|
|
private $options = [self::DEFAULT_FLAGS];
|
|
/**
|
|
* @var \App\Models\Device
|
|
*/
|
|
private $device;
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $abort = false;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->device = DeviceCache::getPrimary();
|
|
}
|
|
|
|
/**
|
|
* Easy way to start a new instance
|
|
*/
|
|
public static function make(): SnmpQueryInterface
|
|
{
|
|
return new static;
|
|
}
|
|
|
|
/**
|
|
* Specify a device to make the snmp query against.
|
|
* By default the query will use the primary device.
|
|
*/
|
|
public function device(Device $device): SnmpQueryInterface
|
|
{
|
|
$this->device = $device;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Specify a device by a device array.
|
|
* The device will be fetched from the cache if it is loaded, otherwise, it will fill the array into a new Device
|
|
*/
|
|
public function deviceArray(array $device): SnmpQueryInterface
|
|
{
|
|
if (isset($device['device_id']) && DeviceCache::has($device['device_id'])) {
|
|
$this->device = DeviceCache::get($device['device_id']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
$this->device = new Device($device);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set a context for the snmp query
|
|
* This is most commonly used to fetch alternate sets of data, such as different VRFs
|
|
*
|
|
* @param string|null $context Version 2/3 context name
|
|
* @param string|null $v3_prefix Optional context prefix to prepend for Version 3 queries
|
|
* @return \LibreNMS\Data\Source\SnmpQueryInterface
|
|
*/
|
|
public function context(?string $context, ?string $v3_prefix = null): SnmpQueryInterface
|
|
{
|
|
if ($context && $this->device->snmpver === 'v3') {
|
|
$context = $v3_prefix . $context;
|
|
}
|
|
|
|
$this->context = $context;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set an additional MIB directory to search for MIBs.
|
|
* You do not need to specify the base and os directories, they are already included.
|
|
*/
|
|
public function mibDir(?string $dir): SnmpQueryInterface
|
|
{
|
|
$this->mibDirs[] = $dir;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* When walking multiple OIDs, stop if one fails. Used when the first OID indicates if the rest are supported.
|
|
* OIDs will be walked in order, so you may want to put your OIDs in a specific order.
|
|
*/
|
|
public function abortOnFailure(): SnmpQueryInterface
|
|
{
|
|
$this->abort = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Do not error on out of order indexes.
|
|
* Use with caution as we could get stuck in an infinite loop.
|
|
*/
|
|
public function allowUnordered(): SnmpQueryInterface
|
|
{
|
|
$this->options = array_merge($this->options, ['-Cc']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Output all OIDs numerically
|
|
*/
|
|
public function numeric(bool $numeric = true): SnmpQueryInterface
|
|
{
|
|
$this->options = $numeric
|
|
? array_merge($this->options, ['-On'])
|
|
: array_diff($this->options, ['-On']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Hide MIB in output
|
|
*/
|
|
public function hideMib(): SnmpQueryInterface
|
|
{
|
|
$this->options = array_merge($this->options, ['-Os']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Output enum values as strings instead of values. This could affect index output.
|
|
*/
|
|
public function enumStrings(): SnmpQueryInterface
|
|
{
|
|
// remove -Oe from the default flags
|
|
if (isset($this->options[0]) && Str::contains($this->options[0], 'e')) {
|
|
$this->options[0] = str_replace('e', '', $this->options[0]);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set option(s) for net-snmp command line. Overrides the default options.
|
|
* Some options may break parsing, but you can manually parse the raw output if needed.
|
|
* This will override other options set such as setting numeric.
|
|
* Calling with null will reset to the default options (-OQXUte).
|
|
* Try to avoid setting options this way to keep the API generic.
|
|
*
|
|
* @param array|string|null $options
|
|
* @return $this
|
|
*/
|
|
public function options($options = []): SnmpQueryInterface
|
|
{
|
|
$this->options = $options !== null
|
|
? Arr::wrap($options)
|
|
: [self::DEFAULT_FLAGS];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* snmpget an OID
|
|
* Commonly used to fetch a single or multiple explicit values.
|
|
*
|
|
* @param array|string $oid
|
|
* @return \LibreNMS\Data\Source\SnmpResponse
|
|
*/
|
|
public function get($oid): SnmpResponse
|
|
{
|
|
return $this->execMultiple('snmpget', $this->limitOids($this->parseOid($oid)));
|
|
}
|
|
|
|
/**
|
|
* snmpwalk an OID
|
|
* Fetches all OIDs under a given OID, commonly used with tables.
|
|
*
|
|
* @param array|string $oid
|
|
* @return \LibreNMS\Data\Source\SnmpResponse
|
|
*/
|
|
public function walk($oid): SnmpResponse
|
|
{
|
|
return $this->execMultiple('snmpwalk', $this->parseOid($oid));
|
|
}
|
|
|
|
/**
|
|
* snmpnext for the given oid
|
|
* snmpnext retrieves the first oid after the given oid.
|
|
*
|
|
* @param array|string $oid
|
|
* @return \LibreNMS\Data\Source\SnmpResponse
|
|
*/
|
|
public function next($oid): SnmpResponse
|
|
{
|
|
return $this->execMultiple('snmpgetnext', $this->limitOids($this->parseOid($oid)));
|
|
}
|
|
|
|
/**
|
|
* Translate an OID.
|
|
* call numeric() on the query to output numeric OID
|
|
*/
|
|
public function translate(string $oid, ?string $mib = null): string
|
|
{
|
|
$this->options = array_diff($this->options, [self::DEFAULT_FLAGS]); // remove default options
|
|
|
|
$this->options[] = '-Pu'; // don't error on _
|
|
|
|
// user did not specify numeric, output full text
|
|
if (! in_array('-On', $this->options)) {
|
|
$this->options[] = '-OS';
|
|
} elseif (Oid::isNumeric($oid)) {
|
|
return Str::start($oid, '.'); // numeric to numeric optimization
|
|
}
|
|
|
|
// if mib is not directly specified and it doesn't have a numeric root
|
|
if (! str_contains($oid, '::') && ! Oid::hasNumericRoot($oid)) {
|
|
$this->options[] = '-IR'; // search for mib
|
|
}
|
|
|
|
if ($mib) {
|
|
array_push($this->options, '-m', $mib);
|
|
}
|
|
|
|
return $this->exec('snmptranslate', [$oid])->value();
|
|
}
|
|
|
|
private function buildCli(string $command, array $oids): array
|
|
{
|
|
$cmd = $this->initCommand($command, $oids);
|
|
|
|
array_push($cmd, '-M', $this->mibDirectories());
|
|
|
|
if ($command === 'snmptranslate') {
|
|
return array_merge($cmd, $this->options, $oids);
|
|
}
|
|
|
|
// authentication
|
|
$this->buildAuth($cmd);
|
|
|
|
$cmd = array_merge($cmd, $this->options);
|
|
|
|
$timeout = $this->device->timeout ?? Config::get('snmp.timeout');
|
|
if ($timeout && $timeout !== 1) {
|
|
array_push($cmd, '-t', $timeout);
|
|
}
|
|
|
|
$retries = $this->device->retries ?? Config::get('snmp.retries');
|
|
if ($retries && $retries !== 5) {
|
|
array_push($cmd, '-r', $retries);
|
|
}
|
|
|
|
$hostname = Rewrite::addIpv6Brackets((string) ($this->device->overwrite_ip ?: $this->device->hostname));
|
|
$cmd[] = ($this->device->transport ?? 'udp') . ':' . $hostname . ':' . $this->device->port;
|
|
|
|
return array_merge($cmd, $oids);
|
|
}
|
|
|
|
private function buildAuth(array &$cmd): void
|
|
{
|
|
if ($this->device->snmpver === 'v3') {
|
|
array_push($cmd, '-v3', '-l', $this->device->authlevel);
|
|
array_push($cmd, '-n', $this->context);
|
|
|
|
switch (strtolower($this->device->authlevel)) {
|
|
case 'authpriv':
|
|
array_push($cmd, '-x', $this->device->cryptoalgo);
|
|
array_push($cmd, '-X', $this->device->cryptopass);
|
|
// fallthrough
|
|
case 'authnopriv':
|
|
array_push($cmd, '-a', $this->device->authalgo);
|
|
array_push($cmd, '-A', $this->device->authpass);
|
|
// fallthrough
|
|
case 'noauthnopriv':
|
|
array_push($cmd, '-u', $this->device->authname ?: 'root');
|
|
break;
|
|
default:
|
|
Log::debug("Unsupported SNMPv3 AuthLevel: {$this->device->snmpver}");
|
|
}
|
|
} elseif ($this->device->snmpver === 'v2c' || $this->device->snmpver === 'v1') {
|
|
array_push($cmd, '-' . $this->device->snmpver, '-c', $this->context ? "{$this->device->community}@$this->context" : $this->device->community);
|
|
} else {
|
|
Log::debug("Unsupported SNMP Version: {$this->device->snmpver}");
|
|
}
|
|
}
|
|
|
|
private function execMultiple(string $command, array $oids): SnmpResponse
|
|
{
|
|
$response = new SnmpResponse('');
|
|
|
|
foreach ($oids as $oid) {
|
|
$response = $response->append($this->exec($command, Arr::wrap($oid)));
|
|
|
|
// 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.');
|
|
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function exec(string $command, array $oids): SnmpResponse
|
|
{
|
|
$measure = Measurement::start($command);
|
|
|
|
$proc = new Process($this->buildCli($command, $oids));
|
|
$proc->setTimeout(Config::get('snmp.exec_timeout', 1200));
|
|
|
|
$this->logCommand($proc->getCommandLine());
|
|
|
|
$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);
|
|
|
|
$measure->manager()->recordSnmp($measure->end());
|
|
|
|
return new SnmpResponse($output, $stderr, $exitCode);
|
|
}
|
|
|
|
private function initCommand(string $binary, array $oids): array
|
|
{
|
|
if ($binary == 'snmpwalk') {
|
|
// allow unordered responses for specific oids
|
|
if (! empty(array_intersect($oids, Config::getCombined($this->device->os, 'oids.unordered', 'snmp.')))) {
|
|
$this->allowUnordered();
|
|
}
|
|
|
|
// handle bulk settings
|
|
if ($this->device->snmpver !== 'v1'
|
|
&& Config::getOsSetting($this->device->os, 'snmp_bulk', true)
|
|
&& empty(array_intersect($oids, Config::getCombined($this->device->os, 'oids.no_bulk', 'snmp.'))) // skip for oids that do not work with bulk
|
|
) {
|
|
$snmpcmd = [Config::get('snmpbulkwalk', 'snmpbulkwalk')];
|
|
|
|
$max_repeaters = $this->device->getAttrib('snmp_max_repeaters') ?: Config::getOsSetting($this->device->os, 'snmp.max_repeaters', Config::get('snmp.max_repeaters', false));
|
|
if ($max_repeaters > 0) {
|
|
$snmpcmd[] = "-Cr$max_repeaters";
|
|
}
|
|
|
|
return $snmpcmd;
|
|
}
|
|
}
|
|
|
|
return [Config::get($binary, $binary)];
|
|
}
|
|
|
|
private function mibDirectories(): string
|
|
{
|
|
$base = Config::get('mib_dir');
|
|
$dirs = [$base];
|
|
|
|
// os group
|
|
if ($os_group = Config::getOsSetting($this->device->os, 'group')) {
|
|
if (file_exists("$base/$os_group")) {
|
|
$dirs[] = "$base/$os_group";
|
|
}
|
|
}
|
|
|
|
// os directory
|
|
if ($os_mibdir = Config::getOsSetting($this->device->os, 'mib_dir')) {
|
|
$dirs[] = "$base/$os_mibdir";
|
|
} elseif (file_exists($base . '/' . $this->device->os)) {
|
|
$dirs[] = $base . '/' . $this->device->os;
|
|
}
|
|
|
|
foreach ($this->mibDirs as $mibDir) {
|
|
$dirs[] = "$base/$mibDir";
|
|
}
|
|
|
|
// remove trailing /, remove empty dirs, and remove duplicates
|
|
$dirs = array_unique(array_filter(array_map(function ($dir) {
|
|
return rtrim($dir, '/');
|
|
}, $dirs)));
|
|
|
|
return implode(':', $dirs);
|
|
}
|
|
|
|
private function checkExitCode(int $code, string $error): void
|
|
{
|
|
if ($code) {
|
|
if (Str::startsWith($error, 'Invalid authentication protocol specified')) {
|
|
Eventlog::log('Unsupported SNMP authentication algorithm - ' . $code, $this->device, 'poller', Alert::ERROR);
|
|
} elseif (Str::startsWith($error, 'Invalid privacy protocol specified')) {
|
|
Eventlog::log('Unsupported SNMP privacy algorithm - ' . $code, $this->device, 'poller', Alert::ERROR);
|
|
}
|
|
Log::debug('Exitcode: ' . $code, [$error]);
|
|
}
|
|
}
|
|
|
|
private function logCommand(string $command): void
|
|
{
|
|
if (Debug::isEnabled() && ! Debug::isVerbose()) {
|
|
$debug_command = preg_replace($this->cleanup['command'][0], $this->cleanup['command'][1], $command);
|
|
Log::debug('SNMP[%c' . $debug_command . '%n]', ['color' => true]);
|
|
} elseif (Debug::isVerbose()) {
|
|
Log::debug('SNMP[%c' . $command . '%n]', ['color' => true]);
|
|
}
|
|
}
|
|
|
|
private function logOutput(string $output, string $error): void
|
|
{
|
|
if (Debug::isEnabled() && ! Debug::isVerbose()) {
|
|
Log::debug(preg_replace($this->cleanup['output'][0], $this->cleanup['output'][1], $output));
|
|
} elseif (Debug::isVerbose()) {
|
|
Log::debug($output);
|
|
}
|
|
Log::debug($error);
|
|
}
|
|
|
|
private function limitOids(array $oids): array
|
|
{
|
|
// get max oids per query device attrib > os setting > global setting
|
|
$configured_max = $this->device->getAttrib('snmp_max_oid') ?: Config::getOsSetting($this->device->os, 'snmp_max_oid', Config::get('snmp.max_oid', 10));
|
|
$max_oids = max($configured_max, 1); // 0 or less would break things.
|
|
|
|
if (count($oids) > $max_oids) {
|
|
return array_chunk($oids, $max_oids);
|
|
}
|
|
|
|
return $oids;
|
|
}
|
|
|
|
private function parseOid(array|string $oid): array
|
|
{
|
|
return is_string($oid) ? explode(' ', $oid) : $oid;
|
|
}
|
|
}
|