Fully convert core to a modern module (#13347)

* Core module WIP

* update OS::make()

* core WIP

* try to finish up

* switch all core do os Model

* Mock WIP

* Working tests

* cleanup

* phpstan fixes

* style fixes

* fix agent

* trim space too
and a couple of cleanups

* corrected ios test data

* missed space

* update test data

* put escapes back

* another net-snmp difference

* Fix class description

* revert snmp.inc.php change, that can be a different PR

* revert snmp.inc.php change, that can be a different PR
This commit is contained in:
Tony Murray
2021-10-19 15:43:43 -05:00
committed by GitHub
parent 17b97abbd2
commit f94f7f23b8
24 changed files with 639 additions and 503 deletions

View File

@@ -37,7 +37,7 @@ use LibreNMS\Util\Rewrite;
use Log;
use Symfony\Component\Process\Process;
class SnmpQuery
class SnmpQuery implements SnmpQueryInterface
{
/**
* @var array
@@ -98,7 +98,7 @@ class SnmpQuery
/**
* Easy way to start a new instance
*/
public static function make(): SnmpQuery
public static function make(): SnmpQueryInterface
{
return new static;
}
@@ -107,7 +107,7 @@ class SnmpQuery
* Specify a device to make the snmp query against.
* By default the query will use the primary device.
*/
public function device(Device $device): SnmpQuery
public function device(Device $device): SnmpQueryInterface
{
$this->device = $device;
@@ -118,7 +118,7 @@ class SnmpQuery
* 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): SnmpQuery
public function deviceArray(array $device): SnmpQueryInterface
{
if (isset($device['device_id']) && DeviceCache::has($device['device_id'])) {
$this->device = DeviceCache::get($device['device_id']);
@@ -135,7 +135,7 @@ class SnmpQuery
* Set a context for the snmp query
* This is most commonly used to fetch alternate sets of data, such as different VRFs
*/
public function context(string $context): SnmpQuery
public function context(string $context): SnmpQueryInterface
{
$this->context = $context;
@@ -146,7 +146,7 @@ class SnmpQuery
* 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): SnmpQuery
public function mibDir(?string $dir): SnmpQueryInterface
{
$this->mibDir = $dir;
@@ -156,7 +156,7 @@ class SnmpQuery
/**
* Output all OIDs numerically
*/
public function numeric(): SnmpQuery
public function numeric(): SnmpQueryInterface
{
$this->options = array_merge($this->options, ['-On']);
@@ -172,7 +172,7 @@ class SnmpQuery
* @param array|string $options
* @return $this
*/
public function options($options = []): SnmpQuery
public function options($options = []): SnmpQueryInterface
{
$this->options = Arr::wrap($options);
@@ -218,10 +218,6 @@ class SnmpQuery
/**
* Translate an OID.
* call numeric() on the query to output numeric OID
*
* @param array|string $oid
* @param string $oid
* @return \LibreNMS\Data\Source\SnmpResponse
*/
public function translate(string $oid, ?string $mib = null): SnmpResponse
{

View File

@@ -0,0 +1,109 @@
<?php
/*
* SnmpQueryInterface.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;
interface SnmpQueryInterface
{
/**
* Easy way to start a new instance
*/
public static function make(): SnmpQueryInterface;
/**
* Specify a device to make the snmp query against.
* By default the query will use the primary device.
*/
public function device(Device $device): SnmpQueryInterface;
/**
* 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;
/**
* Set a context for the snmp query
* This is most commonly used to fetch alternate sets of data, such as different VRFs
*/
public function context(string $context): SnmpQueryInterface;
/**
* 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;
/**
* Output all OIDs numerically
*/
public function numeric(): SnmpQueryInterface;
/**
* Set option(s) for net-snmp command line.
* 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. Call with no options to reset to default.
* Try to avoid setting options this way to keep the API generic.
*
* @param array|string $options
* @return $this
*/
public function options($options = []): SnmpQueryInterface;
/**
* 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;
/**
* 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;
/**
* 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;
/**
* Translate an OID.
* Call numeric method prior output numeric OID.
*/
public function translate(string $oid, ?string $mib = null): SnmpResponse;
}

View File

@@ -1,5 +1,5 @@
<?php
/*
/**
* Core.php
*
* -Description-
@@ -17,37 +17,137 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link https://www.librenms.org
* @copyright 2020 Tony Murray
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Modules;
use App\Models\Device;
use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Interfaces\Module;
use LibreNMS\OS;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Util\Time;
use Log;
use SnmpQuery;
class Core
class Core implements Module
{
public function discover(OS $os)
{
$snmpdata = SnmpQuery::numeric()->get(['SNMPv2-MIB::sysObjectID.0', 'SNMPv2-MIB::sysDescr.0', 'SNMPv2-MIB::sysName.0'])
->values();
$device = $os->getDevice();
$device->fill([
'sysObjectID' => $snmpdata['.1.3.6.1.2.1.1.2.0'] ?? null,
'sysName' => strtolower(trim($snmpdata['.1.3.6.1.2.1.1.5.0'] ?? '')),
'sysDescr' => isset($snmpdata['.1.3.6.1.2.1.1.1.0']) ? str_replace(chr(218), "\n", $snmpdata['.1.3.6.1.2.1.1.1.0']) : null,
]);
foreach ($device->getDirty() as $attribute => $value) {
Log::event($value . ' -> ' . $device->$attribute, $device, 'system', 3);
$os->getDeviceArray()[$attribute] = $value; // update device array
}
// detect OS
$device->os = self::detectOS($device, false);
if ($device->isDirty('os')) {
Log::event('Device OS changed: ' . $device->getOriginal('os') . ' -> ' . $device->os, $device, 'system', 3);
$os->getDeviceArray()['os'] = $device->os;
echo 'Changed ';
}
// Set type to a predefined type for the OS if it's not already set
$loaded_os_type = Config::get("os.$device->os.type");
if (! $device->getAttrib('override_device_type') && $loaded_os_type != $device->type) {
$device->type = $loaded_os_type;
Log::debug("Device type changed to $loaded_os_type!");
}
$device->save();
echo 'OS: ' . Config::getOsSetting($device->os, 'text') . " ($device->os)\n\n";
}
public function poll(OS $os)
{
global $agent_data;
$snmpdata = SnmpQuery::numeric()
->get(['SNMPv2-MIB::sysDescr.0', 'SNMPv2-MIB::sysObjectID.0', 'SNMPv2-MIB::sysUpTime.0', 'SNMPv2-MIB::sysName.0'])
->values();
$device = $os->getDevice();
$device->fill([
'uptime' => $snmpdata['.1.3.6.1.2.1.1.3.0'],
'sysName' => str_replace("\n", '', strtolower($snmpdata['.1.3.6.1.2.1.1.5.0'])),
'sysObjectID' => $snmpdata['.1.3.6.1.2.1.1.2.0'],
'sysDescr' => str_replace(chr(218), "\n", $snmpdata['.1.3.6.1.2.1.1.1.0']),
]);
if (! empty($agent_data['uptime'])) {
[$uptime] = explode(' ', $agent_data['uptime']);
$uptime = round((float) $uptime);
echo "Using UNIX Agent Uptime ($uptime)\n";
} else {
$uptime_data = SnmpQuery::make()->get(['SNMP-FRAMEWORK-MIB::snmpEngineTime.0', 'HOST-RESOURCES-MIB::hrSystemUptime.0'])->values();
$uptime = max(
round($device->uptime / 100),
Config::get("os.$device->os.bad_snmpEngineTime") ? 0 : $uptime_data['SNMP-FRAMEWORK-MIB::snmpEngineTime.0'],
Config::get("os.$device->os.bad_hrSystemUptime") ? 0 : round($uptime_data['HOST-RESOURCES-MIB::hrSystemUptime.0'] / 100)
);
Log::debug("Uptime seconds: $uptime\n");
}
if ($uptime != 0 && Config::get("os.$device->os.bad_uptime") !== true) {
if ($uptime < $device->uptime) {
Log::event('Device rebooted after ' . Time::formatInterval($device->uptime) . " -> {$uptime}s", $device, 'reboot', 4, $device->uptime);
}
app('Datastore')->put($os->getDeviceArray(), 'uptime', [
'rrd_def' => RrdDefinition::make()->addDataset('uptime', 'GAUGE', 0),
], $uptime);
$os->enableGraph('uptime');
echo 'Uptime: ' . Time::formatInterval($uptime) . PHP_EOL;
$device->uptime = $uptime;
$device->save();
}
}
public function cleanup(OS $os)
{
// nothing to cleanup
}
/**
* Detect the os of the given device.
*
* @param array $device device to check
* @param Device $device device to check
* @param bool $fetch fetch sysDescr and sysObjectID fresh from the device
* @return string the name of the os
*
* @throws \Exception
*/
public static function detectOS($device, $fetch = true)
public static function detectOS(Device $device, bool $fetch = true): string
{
if ($fetch) {
// some devices act odd when getting both oids at once
$device['sysDescr'] = snmp_get($device, 'SNMPv2-MIB::sysDescr.0', '-Ovq');
$device['sysObjectID'] = snmp_get($device, 'SNMPv2-MIB::sysObjectID.0', '-Ovqn');
// some devices act oddly when getting both OIDs at once
$device->sysDescr = SnmpQuery::device($device)->get('SNMPv2-MIB::sysDescr.0')->value();
$device->sysObjectID = SnmpQuery::device($device)->numeric()->get('SNMPv2-MIB::sysObjectID.0')->value();
}
d_echo("| {$device['sysDescr']} | {$device['sysObjectID']} | \n");
Log::debug("| $device->sysDescr | $device->sysObjectID | \n");
$deferred_os = [];
$generic_os = [
@@ -96,12 +196,12 @@ class Core
*
* Appending _except to any condition will invert the match.
*
* @param array $device
* @param Device $device
* @param array $array Array of items, keys should be sysObjectID, sysDescr, or sysDescr_regex
* @param string|array $mibdir MIB directory for evaluated OS
* @return bool the result (all items passed return true)
*/
protected static function checkDiscovery($device, $array, $mibdir)
protected static function checkDiscovery(Device $device, array $array, $mibdir): bool
{
// all items must be true
foreach ($array as $key => $value) {
@@ -126,24 +226,20 @@ class Core
return false;
}
} elseif ($key == 'snmpget') {
$get_value = snmp_get(
$device,
$value['oid'],
$value['options'] ?? '-Oqv',
$value['mib'] ?? null,
$value['mib_dir'] ?? $mibdir
);
$get_value = SnmpQuery::device($device)
->options($value['options'] ?? [])
->mibDir($value['mib_dir'] ?? $mibdir)
->get(isset($value['mib']) ? "{$value['mib']}::{$value['oid']}" : $value['oid'])
->value();
if (compare_var($get_value, $value['value'], $value['op'] ?? 'contains') == $check) {
return false;
}
} elseif ($key == 'snmpwalk') {
$walk_value = snmp_walk(
$device,
$value['oid'],
$value['options'] ?? '-Oqv',
$value['mib'] ?? null,
$value['mib_dir'] ?? $mibdir
);
$walk_value = SnmpQuery::device($device)
->options($value['options'] ?? [])
->mibDir($value['mib_dir'] ?? $mibdir)
->walk(isset($value['mib']) ? "{$value['mib']}::{$value['oid']}" : $value['oid'])
->raw();
if (compare_var($walk_value, $value['value'], $value['op'] ?? 'contains') == $check) {
return false;
}
@@ -153,7 +249,7 @@ class Core
return true;
}
protected static function discoveryIsSlow($def)
protected static function discoveryIsSlow($def): bool
{
foreach ($def['discovery'] as $item) {
if (array_key_exists('snmpget', $item) || array_key_exists('snmpwalk', $item)) {

View File

@@ -2,7 +2,7 @@
/**
* OS.php
*
* Base OS class
* -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
@@ -19,7 +19,7 @@
*
* @link https://www.librenms.org
*
* @copyright 2017 Tony Murray
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
@@ -39,6 +39,7 @@ use LibreNMS\OS\Traits\HostResources;
use LibreNMS\OS\Traits\UcdResources;
use LibreNMS\OS\Traits\YamlMempoolsDiscovery;
use LibreNMS\OS\Traits\YamlOSDiscovery;
use LibreNMS\Util\StringHelpers;
class OS implements ProcessorDiscovery, OSDiscovery, MempoolsDiscovery
{
@@ -212,7 +213,7 @@ class OS implements ProcessorDiscovery, OSDiscovery, MempoolsDiscovery
unset($device['os_group']);
}
$class = str_to_class($device['os'], 'LibreNMS\\OS\\');
$class = StringHelpers::toClass($device['os'], 'LibreNMS\\OS\\');
d_echo('Attempting to initialize OS: ' . $device['os'] . PHP_EOL);
if (class_exists($class)) {
d_echo("OS initialized: $class\n");
@@ -221,9 +222,9 @@ class OS implements ProcessorDiscovery, OSDiscovery, MempoolsDiscovery
}
// If not a specific OS, check for a group one.
if ($os_group) {
$class = str_to_class($device['os_group'], 'LibreNMS\\OS\\Shared\\');
d_echo('Attempting to initialize OS: ' . $device['os_group'] . PHP_EOL);
if ($os_group = Config::get("os.{$device['os']}.group")) {
$class = StringHelpers::toClass($os_group, 'LibreNMS\\OS\\Shared\\');
d_echo("Attempting to initialize Group OS: $os_group\n");
if (class_exists($class)) {
d_echo("OS initialized: $class\n");

View File

@@ -125,4 +125,25 @@ class StringHelpers
return $string;
}
/**
* Generate a class name from a lowercase string containing - or _
* Remove - and _ and camel case words
*
* @param string $name The string to convert to a class name
* @param string|null $namespace namespace to prepend to the name for example: LibreNMS\
* @return string Class name
*/
public static function toClass(string $name, ?string $namespace = null): string
{
$pre_format = str_replace(['-', '_'], ' ', $name);
$class = str_replace(' ', '', ucwords(strtolower($pre_format)));
$class = preg_replace_callback('/^(\d)(.)/', function ($matches) {
$numbers = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];
return $numbers[$matches[1]] . strtoupper($matches[2]);
}, $class);
return $namespace . $class;
}
}