New command: lnms report:devices (#15539)
* New report:devices command Print out a list of devices with user specified fields, optionally in csv format * Polish * Apply fixes from StyleCI * Use spaces instead of tab for none type * Fix method call * other commands use whereDeviceSpec * Apply fixes from StyleCI * update command help and back to tab for separator --------- Co-authored-by: StyleCI Bot <bot@styleci.io>
This commit is contained in:
parent
44521ab2f1
commit
183f9559f4
|
@ -32,10 +32,8 @@ use App\Models\Eventlog;
|
|||
use App\Polling\Measure\Measurement;
|
||||
use App\Polling\Measure\MeasurementManager;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
use LibreNMS\Enum\Severity;
|
||||
use LibreNMS\Exceptions\PollerException;
|
||||
use LibreNMS\Polling\ConnectivityHelper;
|
||||
use LibreNMS\Polling\Result;
|
||||
use LibreNMS\RRD\RrdDefinition;
|
||||
|
@ -89,7 +87,7 @@ class Poller
|
|||
|
||||
$this->logger->info("Starting polling run:\n");
|
||||
|
||||
foreach ($this->buildDeviceQuery()->pluck('device_id') as $device_id) {
|
||||
foreach (Device::whereDeviceSpec($this->device_spec)->pluck('device_id') as $device_id) {
|
||||
$results->markAttempted();
|
||||
$this->initDevice($device_id);
|
||||
PollingDevice::dispatch($this->device);
|
||||
|
@ -226,27 +224,6 @@ class Poller
|
|||
return false;
|
||||
}
|
||||
|
||||
private function buildDeviceQuery(): Builder
|
||||
{
|
||||
$query = Device::query();
|
||||
|
||||
if (empty($this->device_spec)) {
|
||||
throw new PollerException('Invalid device spec');
|
||||
} elseif ($this->device_spec == 'all') {
|
||||
return $query;
|
||||
} elseif ($this->device_spec == 'even') {
|
||||
return $query->whereRaw('device_id % 2 = 0');
|
||||
} elseif ($this->device_spec == 'odd') {
|
||||
return $query->whereRaw('device_id % 2 = 1');
|
||||
} elseif (is_numeric($this->device_spec)) {
|
||||
return $query->where('device_id', $this->device_spec);
|
||||
} elseif (Str::contains($this->device_spec, '*')) {
|
||||
return $query->where('hostname', 'like', str_replace('*', '%', $this->device_spec));
|
||||
}
|
||||
|
||||
return $query->where('hostname', $this->device_spec);
|
||||
}
|
||||
|
||||
private function initDevice(int $device_id): void
|
||||
{
|
||||
\DeviceCache::setPrimary($device_id);
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace App\Console\Commands;
|
|||
|
||||
use App\Console\LnmsCommand;
|
||||
use App\Models\Device;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use LibreNMS\Config;
|
||||
use LibreNMS\Polling\ConnectivityHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
@ -32,12 +31,7 @@ class DevicePing extends LnmsCommand
|
|||
public function handle(): int
|
||||
{
|
||||
$spec = $this->argument('device spec');
|
||||
$devices = Device::query()->when($spec !== 'all', function (Builder $query) use ($spec) {
|
||||
/** @phpstan-var Builder<Device> $query */
|
||||
return $query->where('device_id', $spec)
|
||||
->orWhere('hostname', $spec)
|
||||
->limit(1);
|
||||
})->get();
|
||||
$devices = Device::whereDeviceSpec($spec)->get();
|
||||
|
||||
if ($devices->isEmpty()) {
|
||||
$devices = [new Device(['hostname' => $spec])];
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\LnmsCommand;
|
||||
use App\Console\SyntheticDeviceField;
|
||||
use App\Models\Device;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class ReportDevices extends LnmsCommand
|
||||
{
|
||||
protected $name = 'report:devices';
|
||||
const NONE_SEPERATOR = "\t";
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addArgument('device spec', InputArgument::OPTIONAL);
|
||||
$this->addOption('fields', 'f', InputOption::VALUE_REQUIRED, default: 'hostname,ip');
|
||||
$this->addOption('output', 'o', InputOption::VALUE_REQUIRED, __('commands.report:devices.options.output', ['types' => '[table, csv, none]']), 'table');
|
||||
$this->addOption('list-fields');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if ($this->option('list-fields')) {
|
||||
$this->printFields();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$fields = collect(explode(',', $this->option('fields')))->map(fn ($field) => $this->getField($field));
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$headers = $fields->map->headerName()->all();
|
||||
$devices = $this->fetchDeviceData($fields);
|
||||
|
||||
$this->printReport($headers, $devices);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function fetchDeviceData($fields): Collection
|
||||
{
|
||||
$columns = $fields->pluck('columns')->flatten()->all();
|
||||
$query = Device::whereDeviceSpec($this->argument('device spec'))->select($columns);
|
||||
|
||||
// apply any field query modifications
|
||||
foreach ($fields as $field) {
|
||||
$field->modifyQuery($query);
|
||||
}
|
||||
|
||||
// fetch data and call the toString method for each field.
|
||||
return $query->get()->map(function (Device $device) use ($fields) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
$data[$field->name] = $field->toString($device);
|
||||
}
|
||||
|
||||
return $data;
|
||||
});
|
||||
}
|
||||
|
||||
protected function getField(string $field): SyntheticDeviceField
|
||||
{
|
||||
// relationship counts
|
||||
if (str_ends_with($field, '_count')) {
|
||||
[$relationship] = explode('_', $field, -1);
|
||||
if (! (new Device)->isRelation($relationship)) {
|
||||
throw new \Exception("Invalid field: $field");
|
||||
}
|
||||
|
||||
return new SyntheticDeviceField(
|
||||
$field,
|
||||
modifyQuery: fn (Builder $query) => $query->withCount($relationship),
|
||||
headerName: "$relationship count");
|
||||
}
|
||||
|
||||
// misc synthetic fields
|
||||
$syntheticFields = $this->getSyntheticFields();
|
||||
if (isset($syntheticFields[$field])) {
|
||||
return $syntheticFields[$field];
|
||||
}
|
||||
|
||||
// just a regular column, check that it exists
|
||||
if (! Schema::hasColumn('devices', $field)) {
|
||||
throw new \Exception("Invalid field: $field");
|
||||
}
|
||||
|
||||
return new SyntheticDeviceField($field, [$field]);
|
||||
}
|
||||
|
||||
protected function getSyntheticFields(): array
|
||||
{
|
||||
return [
|
||||
'displayName' => new SyntheticDeviceField('displayName', ['hostname', 'sysName', 'ip', 'display'], fn (Device $device) => $device->displayName(), headerName: 'display name'),
|
||||
'location' => new SyntheticDeviceField('location', ['location_id'], fn (Device $device) => $device->location->location, fn (Builder $q) => $q->with('location')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function printReport(array $headers, array|Collection $rows): void
|
||||
{
|
||||
$output = $this->option('output');
|
||||
|
||||
if ($output == 'csv') {
|
||||
$out = fopen('php://output', 'w');
|
||||
fputcsv($out, $headers);
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($out, $row);
|
||||
}
|
||||
fclose($out);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($output == 'none') {
|
||||
foreach ($rows as $row) {
|
||||
$this->line(implode(self::NONE_SEPERATOR, $row));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// print table
|
||||
$this->table($headers, $rows);
|
||||
}
|
||||
|
||||
protected function printFields(): void
|
||||
{
|
||||
$this->info(__('commands.report:devices.columns'));
|
||||
$columns = Schema::getColumnListing('devices');
|
||||
foreach ($columns as $column) {
|
||||
$this->line($column);
|
||||
}
|
||||
|
||||
$this->info(__('commands.report:devices.synthetic'));
|
||||
$synthetic = array_keys($this->getSyntheticFields());
|
||||
foreach ($synthetic as $field) {
|
||||
$this->line($field);
|
||||
}
|
||||
|
||||
$this->info(__('commands.report:devices.counts'));
|
||||
$relationships = Device::definedRelations();
|
||||
foreach ($relationships as $relationship) {
|
||||
$this->line($relationship . '_count');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ namespace App\Console\Commands;
|
|||
use App\Console\LnmsCommand;
|
||||
use App\Models\Device;
|
||||
use DeviceCache;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Rule;
|
||||
use LibreNMS\Data\Source\SnmpResponse;
|
||||
|
@ -149,12 +148,8 @@ abstract class SnmpFetch extends LnmsCommand
|
|||
|
||||
protected function getDevices(): \Illuminate\Support\Collection
|
||||
{
|
||||
return Device::query()->when($this->deviceSpec !== 'all', function (Builder $query) {
|
||||
return $query->where('device_id', $this->deviceSpec)
|
||||
->orWhere('hostname', 'regexp', "^$this->deviceSpec$");
|
||||
})->pluck('device_id')->map(function ($device_id) {
|
||||
return DeviceCache::get($device_id);
|
||||
});
|
||||
return Device::whereDeviceSpec($this->deviceSpec)->pluck('device_id')
|
||||
->map(fn ($device_id) => DeviceCache::get($device_id));
|
||||
}
|
||||
|
||||
protected function fetchData(): SnmpResponse
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
/*
|
||||
* SyntheticDeviceField.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 2023 Tony Murray
|
||||
* @author Tony Murray <murraytony@gmail.com>
|
||||
*/
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Models\Device;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SyntheticDeviceField
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly array $columns = [],
|
||||
public readonly ?Closure $displayFunction = null,
|
||||
public readonly ?Closure $modifyQuery = null,
|
||||
public readonly ?string $headerName = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function headerName(): string
|
||||
{
|
||||
return $this->headerName ?? $this->name;
|
||||
}
|
||||
|
||||
public function modifyQuery(Builder $query): Builder
|
||||
{
|
||||
if ($this->modifyQuery) {
|
||||
return call_user_func($this->modifyQuery, $query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function toString(Device $device): string
|
||||
{
|
||||
if ($this->displayFunction) {
|
||||
return (string) call_user_func($this->displayFunction, $device);
|
||||
}
|
||||
|
||||
return (string) $device->getAttributeValue($this->name);
|
||||
}
|
||||
}
|
|
@ -96,4 +96,20 @@ abstract class BaseModel extends Model
|
|||
->orWhereIntegerInRaw("$table.device_id", \Permissions::devicesForUser($user));
|
||||
});
|
||||
}
|
||||
|
||||
public static function definedRelations(): array
|
||||
{
|
||||
$reflector = new \ReflectionClass(get_called_class());
|
||||
|
||||
return collect($reflector->getMethods())
|
||||
->filter(
|
||||
fn ($method) => ! empty($method->getReturnType()) &&
|
||||
str_contains(
|
||||
$method->getReturnType(),
|
||||
'Illuminate\Database\Eloquent\Relations'
|
||||
)
|
||||
)
|
||||
->pluck('name')
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -628,6 +628,25 @@ class Device extends BaseModel
|
|||
);
|
||||
}
|
||||
|
||||
public function scopeWhereDeviceSpec(Builder $query, ?string $deviceSpec): Builder
|
||||
{
|
||||
if (empty($deviceSpec)) {
|
||||
return $query;
|
||||
} elseif ($deviceSpec == 'all') {
|
||||
return $query;
|
||||
} elseif ($deviceSpec == 'even') {
|
||||
return $query->whereRaw('device_id % 2 = 0');
|
||||
} elseif ($deviceSpec == 'odd') {
|
||||
return $query->whereRaw('device_id % 2 = 1');
|
||||
} elseif (is_numeric($deviceSpec)) {
|
||||
return $query->where('device_id', $deviceSpec);
|
||||
} elseif (str_contains($deviceSpec, '*')) {
|
||||
return $query->where('hostname', 'like', str_replace('*', '%', $deviceSpec));
|
||||
}
|
||||
|
||||
return $query->where('hostname', $deviceSpec);
|
||||
}
|
||||
|
||||
// ---- Define Relationships ----
|
||||
|
||||
public function accessPoints(): HasMany
|
||||
|
|
|
@ -194,6 +194,20 @@ return [
|
|||
'enabled' => ':count plugin enabled|:count plugins enabled',
|
||||
'failed' => 'Failed to enable plugin(s)',
|
||||
],
|
||||
'report:devices' => [
|
||||
'description' => 'Print out data from devices',
|
||||
'columns' => 'Database columns:',
|
||||
'synthetic' => 'Additional fields:',
|
||||
'counts' => 'Relationship counts:',
|
||||
'arguments' => [
|
||||
'device spec' => 'Device spec to poll: device_id, hostname, wildcard (*), odd, even, all',
|
||||
],
|
||||
'options' => [
|
||||
'list-fields' => 'Print out a list of valid fields',
|
||||
'fields' => 'A comma seperated list of fields to display. Valid options: device column names from the database, relationship counts (ports_count), and/or displayName',
|
||||
'output' => 'Output format to display the data :types',
|
||||
],
|
||||
],
|
||||
'smokeping:generate' => [
|
||||
'args-nonsense' => 'Use one of --probes and --targets',
|
||||
'config-insufficient' => 'In order to generate a smokeping configuration, you must have set "smokeping.probes", "fping", and "fping6" set in your configuration',
|
||||
|
@ -216,7 +230,7 @@ return [
|
|||
'snmp:fetch' => [
|
||||
'description' => 'Run snmp query against a device',
|
||||
'arguments' => [
|
||||
'device spec' => 'Device to query: device_id, hostname/ip, hostname regex, or all',
|
||||
'device spec' => 'Device spec to poll: device_id, hostname, wildcard (*), odd, even, all',
|
||||
'oid(s)' => 'One or more SNMP OID to fetch. Should be either MIB::oid or a numeric oid',
|
||||
],
|
||||
'failed' => 'SNMP command failed!',
|
||||
|
|
Loading…
Reference in New Issue