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;
|
|
}
|
|
}
|