OSPF port module (#13498)

* OSPF to module

* Update mock

* lint and style fixes

* enums as strings
This commit is contained in:
Tony Murray
2021-11-12 13:49:09 -06:00
committed by GitHub
parent 666638eeaa
commit 0adf37b4e1
7 changed files with 323 additions and 209 deletions

View File

@@ -39,6 +39,8 @@ use Symfony\Component\Process\Process;
class NetSnmpQuery implements SnmpQueryInterface
{
private const DEFAULT_FLAGS = '-OQXUte';
/**
* @var array
*/
@@ -80,7 +82,7 @@ class NetSnmpQuery implements SnmpQueryInterface
/**
* @var array|string
*/
private $options = ['-OQXUte'];
private $options = [self::DEFAULT_FLAGS];
/**
* @var \App\Models\Device
*/
@@ -174,6 +176,29 @@ class NetSnmpQuery implements SnmpQueryInterface
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.
@@ -188,7 +213,7 @@ class NetSnmpQuery implements SnmpQueryInterface
{
$this->options = $options !== null
? Arr::wrap($options)
: ['-OQXUte'];
: [self::DEFAULT_FLAGS];
return $this;
}

View File

@@ -69,6 +69,16 @@ interface SnmpQueryInterface
*/
public function numeric(): SnmpQueryInterface;
/**
* Hide MIB in output
*/
public function hideMib(): SnmpQueryInterface;
/**
* Output enum values as strings instead of values. This could affect index output.
*/
public function enumStrings(): SnmpQueryInterface;
/**
* Set option(s) for net-snmp command line.
* Some options may break parsing, but you can manually parse the raw output if needed.

View File

@@ -26,6 +26,7 @@
namespace LibreNMS\Data\Source;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Log;
@@ -140,6 +141,16 @@ class SnmpResponse
return $values;
}
public function valuesByIndex(array &$array = []): array
{
foreach ($this->values() as $oid => $value) {
[$name, $index] = array_pad(explode('.', $oid, 2), 2, '');
$array[$index][$name] = $value;
}
return $array;
}
public function table(int $group = 0, array &$array = []): array
{
foreach ($this->values() as $key => $value) {
@@ -159,6 +170,30 @@ class SnmpResponse
return Arr::wrap($array); // if no parts, wrap the value
}
/**
* Map an snmp table with callback.
* Variables passed to the callback will be an array of row values followed by each individual index.
*/
public function mapTable(callable $callback): Collection
{
return collect($this->values())
->map(function ($value, $oid) {
$parts = explode('[', rtrim($oid, ']'), 2);
return [
'_index' => $parts[1] ?? '',
$parts[0] => $value,
];
})
->groupBy('_index')
->map(function ($values, $index) use ($callback) {
$values = array_merge(...$values);
unset($values['_index']);
return call_user_func($callback, $values, ...explode('][', (string) $index));
});
}
/**
* @return int
*/

220
LibreNMS/Modules/Ospf.php Normal file
View File

@@ -0,0 +1,220 @@
<?php
/**
* Ospf.php
*
* Poll OSPF-MIB
*
* 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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Modules;
use App\Models\Ipv4Address;
use App\Models\OspfArea;
use App\Models\OspfInstance;
use App\Models\OspfNbr;
use App\Models\OspfPort;
use App\Observers\ModuleModelObserver;
use LibreNMS\Interfaces\Module;
use LibreNMS\OS;
use LibreNMS\RRD\RrdDefinition;
use SnmpQuery;
class Ospf implements Module
{
/**
* @inheritDoc
*/
public function discover(OS $os): void
{
// no discovery
}
/**
* @inheritDoc
*/
public function poll(OS $os): void
{
foreach ($os->getDevice()->getVrfContexts() as $context_name) {
echo ' Processes: ';
ModuleModelObserver::observe(OspfInstance::class);
// Pull data from device
$ospf_instances_poll = SnmpQuery::context($context_name)
->hideMib()->enumStrings()
->walk('OSPF-MIB::ospfGeneralGroup')->valuesByIndex();
$ospf_instances = collect();
foreach ($ospf_instances_poll as $ospf_instance_id => $ospf_entry) {
$instance = OspfInstance::updateOrCreate([
'device_id' => $os->getDeviceId(),
'ospf_instance_id' => $ospf_instance_id,
'context_name' => $context_name,
], $ospf_entry);
$ospf_instances->push($instance);
}
// cleanup
$os->getDevice()->ospfInstances()
->where('context_name', $context_name)
->whereNotIn('id', $ospf_instances->pluck('id'))->delete();
$instance_count = $ospf_instances->count();
echo $instance_count;
if ($instance_count == 0) {
// if there are no instances, don't check for areas, neighbors, and ports
return;
}
echo ' Areas: ';
ModuleModelObserver::observe(OspfArea::class);
// Pull data from device
$ospf_areas = SnmpQuery::context($context_name)
->hideMib()->enumStrings()
->walk('OSPF-MIB::ospfAreaTable')
->mapTable(function ($ospf_area, $ospf_area_id) use ($context_name, $os) {
return OspfArea::updateOrCreate([
'device_id' => $os->getDeviceId(),
'ospfAreaId' => $ospf_area_id,
'context_name' => $context_name,
], $ospf_area);
});
// cleanup
$os->getDevice()->ospfAreas()
->where('context_name', $context_name)
->whereNotIn('id', $ospf_areas->pluck('id'))->delete();
echo $ospf_areas->count();
echo ' Ports: ';
ModuleModelObserver::observe(OspfPort::class);
// Pull data from device
$ospf_ports = SnmpQuery::context($context_name)
->hideMib()->enumStrings()
->walk('OSPF-MIB::ospfIfTable')
->mapTable(function ($ospf_port, $ip, $ifIndex) use ($context_name, $os) {
// find port_id
$ospf_port['port_id'] = (int) $os->getDevice()->ports()->where('ifIndex', $ifIndex)->value('port_id');
if ($ospf_port['port_id'] == 0) {
$ospf_port['port_id'] = (int) $os->getDevice()->ipv4()
->where('ipv4_address', $ip)
->where('context_name', $context_name)
->value('ipv4_addresses.port_id');
}
return OspfPort::updateOrCreate([
'device_id' => $os->getDeviceId(),
'ospf_port_id' => "$ip.$ifIndex",
'context_name' => $context_name,
], $ospf_port);
});
// cleanup
$os->getDevice()->ospfPorts()
->where('context_name', $context_name)
->whereNotIn('id', $ospf_ports->pluck('id'))->delete();
echo $ospf_ports->count();
echo ' Neighbours: ';
ModuleModelObserver::observe(OspfNbr::class);
// Pull data from device
$ospf_neighbours = SnmpQuery::context($context_name)
->hideMib()->enumStrings()
->walk('OSPF-MIB::ospfNbrTable')
->mapTable(function ($ospf_nbr, $ip, $ifIndex) use ($context_name, $os) {
// get neighbor port_id
$ospf_nbr['port_id'] = Ipv4Address::query()
->where('ipv4_address', $ip)
->where('context_name', $context_name)
->value('port_id');
return OspfNbr::updateOrCreate([
'device_id' => $os->getDeviceId(),
'ospf_nbr_id' => "$ip.$ifIndex",
'context_name' => $context_name,
], $ospf_nbr);
});
// cleanup
$os->getDevice()->ospfNbrs()
->where('context_name', $context_name)
->whereNotIn('id', $ospf_neighbours->pluck('id'))->delete();
echo $ospf_neighbours->count();
echo ' TOS Metrics: ';
// Pull data from device
$ospf_tos_metrics = SnmpQuery::context($context_name)
->hideMib()->enumStrings()
->walk('OSPF-MIB::ospfIfMetricTable')
->mapTable(function ($ospf_tos, $ip) use ($context_name, $os) {
$ospf_tos['ospf_port_id'] = OspfPort::query()
->where('ospfIfIpAddress', $ip)
->where('context_name', $context_name)
->value('ospf_port_id');
return OspfPort::updateOrCreate([
'device_id' => $os->getDeviceId(),
'ospf_port_id' => $ospf_tos['ospf_port_id'],
'context_name' => $context_name,
], $ospf_tos);
});
echo $ospf_tos_metrics->count();
echo PHP_EOL;
if ($instance_count) {
// Create device-wide statistics RRD
$rrd_def = RrdDefinition::make()
->addDataset('instances', 'GAUGE', 0, 1000000)
->addDataset('areas', 'GAUGE', 0, 1000000)
->addDataset('ports', 'GAUGE', 0, 1000000)
->addDataset('neighbours', 'GAUGE', 0, 1000000);
$fields = [
'instances' => $instance_count,
'areas' => $ospf_areas->count(),
'ports' => $ospf_ports->count(),
'neighbours' => $ospf_neighbours->count(),
];
$tags = compact('rrd_def');
app('Datastore')->put($os->getDeviceArray(), 'ospf-statistics', $tags, $fields);
}
}
}
/**
* @inheritDoc
*/
public function cleanup(OS $os): void
{
$os->getDevice()->ospfPorts()->delete();
$os->getDevice()->ospfNbrs()->delete();
$os->getDevice()->ospfAreas()->delete();
$os->getDevice()->ospfInstances()->delete();
}
}

View File

@@ -700,6 +700,11 @@ class Device extends BaseModel
return $this->hasMany(\App\Models\MuninPlugin::class, 'device_id');
}
public function ospfAreas(): HasMany
{
return $this->hasMany(\App\Models\OspfArea::class, 'device_id');
}
public function ospfInstances(): HasMany
{
return $this->hasMany(\App\Models\OspfInstance::class, 'device_id');

View File

@@ -1,211 +1,8 @@
<?php
use App\Models\Ipv4Address;
use App\Models\OspfArea;
use App\Models\OspfInstance;
use App\Models\OspfNbr;
use App\Models\OspfPort;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\OS;
$device_model = DeviceCache::getPrimary();
foreach ($device_model->getVrfContexts() as $context_name) {
$device['context_name'] = $context_name;
echo ' Processes: ';
// Pull data from device
$ospf_instances_poll = snmpwalk_cache_oid($device, 'OSPF-MIB::ospfGeneralGroup', [], 'OSPF-MIB');
d_echo($ospf_instances_poll);
$ospf_instances = collect();
foreach ($ospf_instances_poll as $ospf_instance_id => $ospf_entry) {
// TODO add model listener from wireless polling PR #8607 for improved output
$instance = OspfInstance::updateOrCreate([
'device_id' => $device['device_id'],
'ospf_instance_id' => $ospf_instance_id,
'context_name' => $device['context_name'],
], $ospf_entry);
$ospf_instances->push($instance);
}
// cleanup
OspfInstance::query()
->where(['device_id' => $device['device_id'], 'context_name' => $device['context_name']])
->whereNotIn('id', $ospf_instances->pluck('id'))->delete();
$instance_count = $ospf_instances->count();
echo $instance_count;
if ($instance_count == 0) {
// if there are no instances, don't check for areas, neighbors, and ports
return;
}
echo ' Areas: ';
// Pull data from device
$ospf_areas_poll = snmpwalk_cache_oid($device, 'OSPF-MIB::ospfAreaEntry', [], 'OSPF-MIB');
d_echo($ospf_areas_poll);
$ospf_areas = collect();
foreach ($ospf_areas_poll as $ospf_area_id => $ospf_area) {
$area = OspfArea::updateOrCreate([
'device_id' => $device['device_id'],
'ospfAreaId' => $ospf_area_id,
'context_name' => $device['context_name'],
], $ospf_area);
$ospf_areas->push($area);
}
// cleanup
OspfArea::query()
->where(['device_id' => $device['device_id'], 'context_name' => $device['context_name']])
->whereNotIn('id', $ospf_areas->pluck('id'))->delete();
echo $ospf_areas->count();
echo ' Ports: ';
// Pull data from device
$ospf_ports_poll = snmpwalk_cache_oid($device, 'OSPF-MIB::ospfIfEntry', [], 'OSPF-MIB');
d_echo($ospf_ports_poll);
$ospf_ports = collect();
foreach ($ospf_ports_poll as $ospf_port_id => $ospf_port) {
// find port_id
if ($ospf_port['ospfAddressLessIf']) {
$ospf_port['port_id'] = (int) $device_model->ports()->where('ifIndex', $ospf_port['ospfAddressLessIf'])->value('port_id');
} else {
// FIXME force same device ?
$ospf_port['port_id'] = (int) Ipv4Address::query()
->where('ipv4_address', $ospf_port['ospfIfIpAddress'])
->where('context_name', $device['context_name'])
->value('port_id');
}
$port = OspfPort::updateOrCreate([
'device_id' => $device['device_id'],
'ospf_port_id' => $ospf_port_id,
'context_name' => $device['context_name'],
], $ospf_port);
$ospf_ports->push($port);
}
// cleanup
OspfPort::query()
->where(['device_id' => $device['device_id'], 'context_name' => $device['context_name']])
->whereNotIn('id', $ospf_ports->pluck('id'))->delete();
echo $ospf_ports->count();
echo ' Neighbours: ';
// Pull data from device
$ospf_nbrs_poll = snmpwalk_cache_oid($device, 'OSPF-MIB::ospfNbrEntry', [], 'OSPF-MIB');
d_echo($ospf_nbrs_poll);
$ospf_neighbours = collect();
foreach ($ospf_nbrs_poll as $ospf_nbr_id => $ospf_nbr) {
// get neighbor port_id
$ospf_nbr['port_id'] = Ipv4Address::query()
->where('ipv4_address', $ospf_nbr['ospfNbrIpAddr'])
->where('context_name', $device['context_name'])
->value('port_id');
$ospf_nbr['ospf_nbr_id'] = $ospf_nbr_id;
$neighbour = OspfNbr::updateOrCreate([
'device_id' => $device['device_id'],
'ospf_nbr_id' => $ospf_nbr_id,
'context_name' => $device['context_name'],
], $ospf_nbr);
$ospf_neighbours->push($neighbour);
}
// cleanup
OspfNbr::query()
->where(['device_id' => $device['device_id'], 'context_name' => $device['context_name']])
->whereNotIn('id', $ospf_neighbours->pluck('id'))->delete();
echo $ospf_neighbours->count();
echo ' TOS Metrics: ';
// Pull data from device
$ospf_tos_poll = snmpwalk_cache_oid($device, 'OSPF-MIB::ospfIfMetricEntry', [], 'OSPF-MIB');
d_echo($ospf_tos_poll);
$ospf_tos_metrics = collect();
foreach ($ospf_tos_poll as $ospf_tos_id => $ospf_tos) {
// get ospf_port_id
$ospf_tos['ospf_port_id'] = OspfPort::query()
->where('ospfIfIpAddress', $ospf_tos['ospfIfMetricIpAddress'])
->where('context_name', $device['context_name'])
->value('ospf_port_id');
$tos = OspfPort::updateOrCreate([
'device_id' => $device['device_id'],
'ospf_port_id' => $ospf_tos['ospf_port_id'],
'context_name' => $device['context_name'],
], $ospf_tos);
$ospf_tos_metrics->push($tos);
}
echo $ospf_tos_metrics->count();
if (! $os instanceof OS) {
$os = OS::make($device);
}
unset($device['context_name'], $context_name);
if ($instance_count) {
// Create device-wide statistics RRD
$rrd_def = RrdDefinition::make()
->addDataset('instances', 'GAUGE', 0, 1000000)
->addDataset('areas', 'GAUGE', 0, 1000000)
->addDataset('ports', 'GAUGE', 0, 1000000)
->addDataset('neighbours', 'GAUGE', 0, 1000000);
$fields = [
'instances' => $instance_count,
'areas' => $ospf_areas->count(),
'ports' => $ospf_ports->count(),
'neighbours' => $ospf_neighbours->count(),
];
$tags = compact('rrd_def');
data_update($device, 'ospf-statistics', $tags, $fields);
}
echo PHP_EOL;
unset(
$ospf_instances,
$instance_count,
$ospf_areas,
$ospf_ports,
$ospf_neighbours,
$ospf_instances_poll,
$ospf_areas_poll,
$ospf_ports_poll,
$ospf_nbrs_poll,
$ospf_entry,
$instance,
$ospf_instance_id,
$ospf_area,
$area,
$ospf_area_id,
$ospf_port,
$port,
$ospf_port_id,
$ospf_nbr,
$neighbour,
$ospf_nbr_id,
$ospf_tos,
$tos,
$ospf_tos_id,
$rrd_def,
$fields,
$tags
);
(new \LibreNMS\Modules\Ospf())->poll($os);

View File

@@ -58,6 +58,10 @@ class SnmpQueryMock implements SnmpQueryInterface
* @var bool
*/
private $numeric = false;
/**
* @var bool
*/
private $hideMib = false;
/**
* @var array|mixed
*/
@@ -99,6 +103,9 @@ class SnmpQueryMock implements SnmpQueryInterface
if ($this->numeric) {
$options[] = '-On';
}
if ($this->hideMib) {
$options[] = '-Os';
}
return NetSnmpQuery::make()
->mibDir($this->mibDir)
@@ -118,6 +125,21 @@ class SnmpQueryMock implements SnmpQueryInterface
return $this;
}
public function hideMib(): SnmpQueryInterface
{
$this->hideMib = true;
return $this;
}
public function enumStrings(): SnmpQueryInterface
{
// TODO: Implement enumStrings() method, no idea how
Log::error('enumStrings not implemented in SnmpQueryMock');
return $this;
}
public function options($options = []): SnmpQueryInterface
{
$this->options = $options;