Poller rewrite (Try 2) (#13525)

* core WIP

* try to finish up

* trim space too
and a couple of cleanups

* update test data

* put escapes back

* another net-snmp difference

* correct copy paste error

* WIP

* Use new code YAY

* a tiny bit more

* Kind of working

* Handle manual modules correctly

* convert core to modern module

* Only save metrics if modules is not overridden

* correct module exists check

* database error handling

* debug handling

* restore bad changes

* Introduce Actions
 RunAlertRulesAction
 UpdateDeviceGroupsAction

* tweaks to output

* Fix some issues in outside code

* Style fixes

* fixes to module status checks

* typehints!

* Use logger only and DI

* OS module not named correctly

* Work on quiet output a bit more

* generically don't change output when disabling debug if the driver is already stack

* Fix missing $device variable for legacy os polling
Fix missing dbFacile functions when no legacy modules polled in RunAlertRulesAction

* restore legacy os module shim

* use the new poller code for tests

* PollingDevice event

* Fix some issues and enable/disable error reporting around legacy modules

* typehints

* fully update baseline

* Use Process for version commands so we don't leak debug output.

* don't detect rrdtool version in ci every time

* style fixes

* Warning fixes

* more fixes

* re-update baseline

* remove diff noise

* fix up alerts

* Catch exceptions in device ip lookup

* Revert accidental snmp.inc.php poller target change
(should have been ?: not ??)
This commit is contained in:
Tony Murray 2021-11-17 19:23:55 -06:00 committed by GitHub
parent 7893b8bebe
commit c79b187d72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1187 additions and 305 deletions

View File

@ -33,6 +33,7 @@ namespace LibreNMS\Alert;
use Carbon\Carbon;
use LibreNMS\Enum\AlertState;
use Log;
class AlertRules
{
@ -57,7 +58,7 @@ class AlertRules
}
//Checks each rule.
foreach (AlertUtil::getRules($device_id) as $rule) {
c_echo('Rule %p#' . $rule['id'] . ' (' . $rule['name'] . '):%n ');
Log::info('Rule %p#' . $rule['id'] . ' (' . $rule['name'] . '):%n ', ['color' => true]);
$extra = json_decode($rule['extra'], true);
if (isset($extra['invert'])) {
$inv = (bool) $extra['invert'];
@ -90,9 +91,9 @@ class AlertRules
$current_state = dbFetchCell('SELECT state FROM alerts WHERE rule_id = ? AND device_id = ? ORDER BY id DESC LIMIT 1', [$rule['id'], $device_id]);
if ($doalert) {
if ($current_state == AlertState::ACKNOWLEDGED) {
c_echo('Status: %ySKIP');
Log::info('Status: %ySKIP%n', ['color' => true]);
} elseif ($current_state >= AlertState::ACTIVE) {
c_echo('Status: %bNOCHG');
Log::info('Status: %bNOCHG%n', ['color' => true]);
// NOCHG here doesn't mean no change full stop. It means no change to the alert state
// So we update the details column with any fresh changes to the alert output we might have.
$alert_log = dbFetchRow('SELECT alert_log.id, alert_log.details FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0
@ -113,12 +114,12 @@ class AlertRules
} else {
dbUpdate(['state' => AlertState::ACTIVE, 'open' => 1, 'timestamp' => Carbon::now()], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
}
c_echo(PHP_EOL . 'Status: %rALERT');
Log::info(PHP_EOL . 'Status: %rALERT%n', ['color' => true]);
}
}
} else {
if (! is_null($current_state) && $current_state == AlertState::RECOVERED) {
c_echo('Status: %bNOCHG');
Log::info('Status: %bNOCHG%n', ['color' => true]);
} else {
if (dbInsert(['state' => AlertState::RECOVERED, 'device_id' => $device_id, 'rule_id' => $rule['id']], 'alert_log')) {
if (is_null($current_state)) {
@ -127,11 +128,10 @@ class AlertRules
dbUpdate(['state' => AlertState::RECOVERED, 'open' => 1, 'note' => '', 'timestamp' => Carbon::now()], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
}
c_echo(PHP_EOL . 'Status: %gOK');
Log::info(PHP_EOL . 'Status: %gOK%n', ['color' => true]);
}
}
}
c_echo('%n' . PHP_EOL);
}
}
}

View File

@ -51,6 +51,14 @@ class Device
$this->primary = $device_id;
}
/**
* Check if a primary device is set
*/
public function hasPrimary(): bool
{
return $this->primary !== null;
}
/**
* Get a device by device_id
*

View File

@ -168,7 +168,7 @@ class Rrd extends BaseDatastore
$fields = array_filter($fields, function ($key) use ($rrd_def) {
$valid = $rrd_def->isValidDataset($key);
if (! $valid) {
Log::warning("RRD warning: unused data sent $key");
Log::debug("RRD warning: unused data sent $key");
}
return $valid;

View File

@ -0,0 +1,30 @@
<?php
/*
* PollerException.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\Exceptions;
class PollerException extends \Exception
{
}

View File

@ -33,7 +33,7 @@ interface Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function discover(OS $os);
@ -42,7 +42,7 @@ interface Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os);
@ -50,7 +50,7 @@ interface Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function cleanup(OS $os);
}

View File

@ -51,7 +51,7 @@ class Isis implements Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function discover(OS $os)
{
@ -68,7 +68,7 @@ class Isis implements Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os)
{
@ -89,7 +89,7 @@ class Isis implements Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param Os $os
*/
public function cleanup(OS $os)
{

View File

@ -0,0 +1,65 @@
<?php
/**
* LegacyModule.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Modules;
use LibreNMS\Interfaces\Module;
use LibreNMS\OS;
use LibreNMS\Util\Debug;
class LegacyModule implements Module
{
/**
* @var string
*/
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function discover(OS $os): void
{
// TODO: Implement discover() method.
}
public function poll(OS $os): void
{
$device = &$os->getDeviceArray();
$device['attribs'] = $os->getDevice()->attribs->toArray();
Debug::disableErrorReporting(); // ignore errors in legacy code
include_once base_path('includes/dbFacile.php');
include base_path("includes/polling/$this->name.inc.php");
Debug::enableErrorReporting(); // and back to normal
}
public function cleanup(OS $os): void
{
// TODO: Implement cleanup() method.
}
}

View File

@ -108,7 +108,7 @@ class Mempools implements Module
}
/**
* @param OS $os
* @param \LibreNMS\OS $os
* @param \Illuminate\Support\Collection $mempools
* @return \Illuminate\Support\Collection
*/

View File

@ -42,7 +42,7 @@ class Mpls implements Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function discover(OS $os)
{
@ -88,7 +88,7 @@ class Mpls implements Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os)
{
@ -151,7 +151,7 @@ class Mpls implements Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param Os $os
*/
public function cleanup(OS $os)
{

View File

@ -37,7 +37,7 @@ class Nac implements Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param Os $os
*/
public function discover(OS $os)
{
@ -49,7 +49,7 @@ class Nac implements Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os)
{
@ -82,7 +82,7 @@ class Nac implements Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function cleanup(OS $os)
{

View File

@ -30,9 +30,9 @@ use LibreNMS\Interfaces\Module;
use LibreNMS\Interfaces\Polling\OSPolling;
use LibreNMS\Util\Url;
class OS implements Module
class Os implements Module
{
public function discover(\LibreNMS\OS $os)
public function discover(\LibreNMS\OS $os): void
{
$this->updateLocation($os);
$this->sysContact($os);
@ -50,7 +50,7 @@ class OS implements Module
$this->handleChanges($os);
}
public function poll(\LibreNMS\OS $os)
public function poll(\LibreNMS\OS $os): void
{
$deviceModel = $os->getDevice(); /** @var \App\Models\Device $deviceModel */
if ($os instanceof OSPolling) {
@ -58,6 +58,11 @@ class OS implements Module
} else {
// legacy poller files
global $graphs, $device;
if (empty($device)) {
$device = $os->getDeviceArray();
}
$location = null;
if (is_file(base_path('/includes/polling/os/' . $device['os'] . '.inc.php'))) {
@ -85,12 +90,12 @@ class OS implements Module
$this->handleChanges($os);
}
public function cleanup(\LibreNMS\OS $os)
public function cleanup(\LibreNMS\OS $os): void
{
// no cleanup needed?
}
private function handleChanges(\LibreNMS\OS $os)
private function handleChanges(\LibreNMS\OS $os): void
{
$device = $os->getDevice();
@ -104,7 +109,7 @@ class OS implements Module
$device->save();
}
private function updateLocation(\LibreNMS\OS $os)
private function updateLocation(\LibreNMS\OS $os): void
{
$device = $os->getDevice();
$new_location = $device->override_sysLocation ? new Location() : $os->fetchLocation(); // fetch location data from device
@ -112,7 +117,7 @@ class OS implements Module
optional($device->location)->save();
}
private function sysContact(\LibreNMS\OS $os)
private function sysContact(\LibreNMS\OS $os): void
{
$device = $os->getDevice();
$device->sysContact = snmp_get($os->getDeviceArray(), 'sysContact.0', '-Ovq', 'SNMPv2-MIB');

View File

@ -39,7 +39,7 @@ class PrinterSupplies implements Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function discover(OS $os)
{
@ -58,7 +58,7 @@ class PrinterSupplies implements Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os)
{
@ -114,7 +114,7 @@ class PrinterSupplies implements Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param Os $os
*/
public function cleanup(OS $os)
{

View File

@ -36,7 +36,7 @@ class Slas implements Module
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function discover(OS $os)
{
@ -52,7 +52,7 @@ class Slas implements Module
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function poll(OS $os)
{
@ -73,7 +73,7 @@ class Slas implements Module
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
* @param \LibreNMS\OS $os
*/
public function cleanup(OS $os)
{

View File

@ -127,7 +127,7 @@ class OS implements
$this->graphs[$name] = true;
}
public function persistGraphs()
public function persistGraphs(): void
{
$device = $this->getDevice();
$graphs = collect(array_keys($this->graphs));

374
LibreNMS/Poller.php Normal file
View File

@ -0,0 +1,374 @@
<?php
/**
* Poller.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS;
use App\Events\DevicePolled;
use App\Events\PollingDevice;
use App\Models\Device;
use App\Polling\Measure\Measurement;
use App\Polling\Measure\MeasurementManager;
use Carbon\Carbon;
use DB;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use LibreNMS\Exceptions\PollerException;
use LibreNMS\Modules\LegacyModule;
use LibreNMS\Polling\ConnectivityHelper;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Dns;
use LibreNMS\Util\Git;
use LibreNMS\Util\StringHelpers;
use Psr\Log\LoggerInterface;
class Poller
{
/** @var string */
private $device_spec;
/** @var array */
private $module_override;
/**
* @var Device
*/
private $device;
/**
* @var array
*/
private $deviceArray;
/**
* @var \LibreNMS\OS|\LibreNMS\OS\Generic
*/
private $os;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(string $device_spec, array $module_override, LoggerInterface $logger)
{
$this->device_spec = $device_spec;
$this->module_override = $module_override;
$this->logger = $logger;
$this->parseModules();
}
public function poll(): int
{
$polled = 0;
$this->printHeader();
if (Debug::isEnabled()) {
\LibreNMS\Util\OS::updateCache(true); // Force update of OS Cache
}
$this->logger->info("Starting polling run:\n");
foreach ($this->buildDeviceQuery()->pluck('device_id') as $device_id) {
$this->initDevice($device_id);
PollingDevice::dispatch($this->device);
$this->os = OS::make($this->deviceArray);
$helper = new ConnectivityHelper($this->device);
$helper->saveMetrics();
$measurement = Measurement::start('poll');
$measurement->manager()->checkpoint(); // don't count previous stats
if ($helper->isUp()) {
$this->pollModules();
}
$measurement->end();
if (empty($this->module_override)) {
// record performance
$measurement->manager()->record('device', $measurement);
$this->device->last_polled = Carbon::now();
$this->device->last_ping_timetaken = $measurement->getDuration();
app('Datastore')->put($this->deviceArray, 'poller-perf', [
'rrd_def' => RrdDefinition::make()->addDataset('poller', 'GAUGE', 0),
'module' => 'ALL',
], [
'poller' => $measurement->getDuration(),
]);
$this->os->enableGraph('poller_perf');
if ($helper->canPing()) {
$this->os->enableGraph('ping_perf');
}
$this->os->persistGraphs();
$this->logger->info(sprintf("Enabled graphs (%s): %s\n\n",
$this->device->graphs->count(),
$this->device->graphs->pluck('graph')->implode(' ')
));
}
$this->device->save();
$polled++;
DevicePolled::dispatch($this->device);
$this->logger->info(sprintf("\n>>> Polled %s (%s) in %0.3f seconds <<<",
$this->device->displayName(),
$this->device->device_id,
$measurement->getDuration()));
// check if the poll took too long and log an event
if ($measurement->getDuration() > Config::get('rrd.step')) {
\Log::event('Polling took longer than ' . round(Config::get('rrd.step') / 60, 2) .
' minutes! This will cause gaps in graphs.', $this->device, 'system', 5);
}
}
return $polled;
}
private function pollModules(): void
{
$this->filterModules();
// update $device array status
$this->deviceArray['status'] = $this->device->status;
$this->deviceArray['status_reason'] = $this->device->status_reason;
// import legacy garbage
include_once base_path('includes/functions.php');
include_once base_path('includes/common.php');
include_once base_path('includes/polling/functions.inc.php');
include_once base_path('includes/snmp.inc.php');
include_once base_path('includes/datastore.inc.php'); // remove me
foreach (Config::get('poller_modules') as $module => $module_status) {
if ($this->isModuleEnabled($module, $module_status)) {
$start_memory = memory_get_usage();
$module_start = microtime(true);
$this->logger->info("\n#### Load poller module $module ####");
try {
$module_class = StringHelpers::toClass($module, '\\LibreNMS\\Modules\\');
$instance = class_exists($module_class) ? new $module_class : new LegacyModule($module);
$instance->poll($this->os);
} catch (Exception $e) {
// isolate module exceptions so they don't disrupt the polling process
$this->logger->error("Error in $module module. " . $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL);
}
app(MeasurementManager::class)->printChangedStats();
$this->saveModulePerformance($module, $module_start, $start_memory);
$this->logger->info("#### Unload poller module $module ####\n");
}
}
}
private function saveModulePerformance(string $module, float $start_time, int $start_memory): void
{
$module_time = microtime(true) - $start_time;
$module_mem = (memory_get_usage() - $start_memory);
$this->logger->info(sprintf(">> Runtime for poller module '%s': %.4f seconds with %s bytes", $module, $module_time, $module_mem));
app('Datastore')->put($this->deviceArray, 'poller-perf', [
'module' => $module,
'rrd_def' => RrdDefinition::make()->addDataset('poller', 'GAUGE', 0),
'rrd_name' => ['poller-perf', $module],
], [
'poller' => $module_time,
]);
$this->os->enableGraph('poller_modules_perf');
}
private function isModuleEnabled(string $module, bool $global_status): bool
{
if (! empty($this->module_override)) {
if (in_array($module, $this->module_override)) {
$this->logger->debug("Module $module manually enabled");
return true;
}
return false;
}
$os_module_status = Config::get("os.{$this->device->os}.poller_modules.$module");
$device_attrib = $this->device->getAttrib('poll_' . $module);
$this->logger->debug(sprintf('Modules status: Global %s OS %s Device %s',
$global_status ? '+' : '-',
$os_module_status === null ? ' ' : ($os_module_status ? '+' : '-'),
$device_attrib === null ? ' ' : ($device_attrib ? '+' : '-')
));
if ($device_attrib
|| ($os_module_status && $device_attrib === null)
|| ($global_status && $os_module_status === null && $device_attrib === null)) {
return true;
}
$reason = $device_attrib !== null ? 'by device'
: ($os_module_status === null || $os_module_status ? 'globally' : 'by OS');
$this->logger->debug("Module [ $module ] disabled $reason");
return false;
}
private function moduleExists(string $module): bool
{
return class_exists(StringHelpers::toClass($module, '\\LibreNMS\\Modules\\'))
|| is_file("includes/polling/$module.inc.php");
}
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->where(DB::raw('device_id % 2'), 0);
} elseif ($this->device_spec == 'odd') {
return $query->where(DB::raw('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);
$this->device = \DeviceCache::getPrimary();
$this->device->ip = $this->device->overwrite_ip ?: Dns::lookupIp($this->device);
$this->deviceArray = $this->device->toArray();
if ($os_group = Config::get("os.{$this->device->os}.group")) {
$this->deviceArray['os_group'] = $os_group;
}
$this->printDeviceInfo($os_group);
$this->initRrdDirectory();
}
private function initRrdDirectory(): void
{
$host_rrd = \Rrd::name($this->device->hostname, '', '');
if (Config::get('rrd.enable', true) && ! is_dir($host_rrd)) {
mkdir($host_rrd);
$this->logger->info("Created directory : $host_rrd");
}
}
private function parseModules(): void
{
foreach ($this->module_override as $index => $module) {
// parse submodules (only supported by some modules)
if (Str::contains($module, '/')) {
[$module, $submodule] = explode('/', $module, 2);
$existing_submodules = Config::get("poller_submodules.$module", []);
$existing_submodules[] = $submodule;
Config::set("poller_submodules.$module", $existing_submodules);
}
if (! $this->moduleExists($module)) {
unset($this->module_override[$index]);
continue;
}
Config::set("poller_modules.$module", 1);
}
$this->printModules();
}
private function filterModules(): void
{
if ($this->device->snmp_disable) {
// only non-snmp modules
Config::set('poller_modules', array_intersect_key(Config::get('poller_modules'), [
'availability' => true,
'ipmi' => true,
'unix-agent' => true,
]));
} else {
// we always want the core module to be included, prepend it
Config::set('poller_modules', ['core' => true] + Config::get('poller_modules'));
}
}
private function printDeviceInfo(?string $group): void
{
$this->logger->info(sprintf(<<<'EOH'
Hostname: %s %s
ID: %s
OS: %s
IP: %s
EOH, $this->device->hostname, $group ? " ($group)" : '', $this->device->device_id, $this->device->os, $this->device->ip));
}
private function printModules(): void
{
$modules = array_map(function ($module) {
$submodules = Config::get("poller_submodules.$module");
return $module . ($submodules ? '(' . implode(',', $submodules) . ')' : '');
}, array_keys(Config::get('poller_modules', [])));
$this->logger->debug('Override poller modules: ' . implode(', ', $modules));
}
private function printHeader(): void
{
if (Debug::isEnabled() || Debug::isVerbose()) {
$version = \LibreNMS\Util\Version::get();
$this->logger->info(sprintf(<<<'EOH'
===================================
Version info:
Commit SHA: %s
Commit Date: %s
DB Schema: %s
PHP: %s
MySQL: %s
RRDTool: %s
SNMP: %s
==================================
EOH,
Git::localCommit(),
Git::localDate(),
vsprintf('%s (%s)', $version->database()),
phpversion(),
\LibreNMS\DB\Eloquent::isConnected() ? \LibreNMS\DB\Eloquent::version() : '?',
$version->rrdtool(),
$version->netSnmp()
));
}
}
}

View File

@ -32,7 +32,13 @@ use Log;
class Debug
{
/**
* @var bool
*/
private static $debug = false;
/**
* @var bool
*/
private static $verbose = false;
/**
@ -49,20 +55,12 @@ class Debug
restore_error_handler(); // disable Laravel error handler
if (self::$debug) {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '0');
error_reporting(E_ALL & ~E_NOTICE);
self::enableErrorReporting();
self::enableCliDebugOutput();
self::enableQueryDebug();
} else {
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('log_errors', '1');
error_reporting($silence ? 0 : E_ERROR);
self::disableCliDebugOutput();
self::disableErrorReporting($silence);
self::disableCliDebugOutput($silence);
self::disableQueryDebug();
}
@ -92,7 +90,7 @@ class Debug
return self::$verbose;
}
public static function disableQueryDebug()
public static function disableQueryDebug(): void
{
$db = Eloquent::DB();
@ -102,21 +100,21 @@ class Debug
}
}
public static function enableCliDebugOutput()
public static function enableCliDebugOutput(): void
{
if (Laravel::isBooted() && App::runningInConsole()) {
Log::setDefaultDriver('console');
Log::setDefaultDriver('console_debug');
}
}
public static function disableCliDebugOutput()
public static function disableCliDebugOutput(bool $silence): void
{
if (Laravel::isBooted()) {
Log::setDefaultDriver('stack');
if (Laravel::isBooted() && Log::getDefaultDriver() !== 'stack') {
Log::setDefaultDriver(app()->runningInConsole() && ! $silence ? 'console' : 'stack');
}
}
public static function enableQueryDebug()
public static function enableQueryDebug(): void
{
static $sql_debug_enabled;
$db = Eloquent::DB();
@ -141,4 +139,26 @@ class Debug
$sql_debug_enabled = true;
}
}
/**
* Disable error reporting, do not use with new code
*/
public static function disableErrorReporting(bool $silence = false): void
{
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('log_errors', '1');
error_reporting($silence ? 0 : E_ERROR);
}
/**
* Enable error reporting. Please call after disabling for legacy code
*/
public static function enableErrorReporting(): void
{
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '0');
error_reporting(E_ALL & ~E_NOTICE);
}
}

View File

@ -25,6 +25,7 @@
namespace LibreNMS\Util;
use App\Models\Device;
use LibreNMS\Interfaces\Geocoder;
class Dns implements Geocoder
@ -36,6 +37,23 @@ class Dns implements Geocoder
$this->resolver = new \Net_DNS2_Resolver();
}
public static function lookupIp(Device $device): ?string
{
if (IP::isValid($device->hostname)) {
return $device->hostname;
}
try {
if ($device->transport == 'udp6' || $device->transport == 'tcp6') {
return dns_get_record($device['hostname'], DNS_AAAA)[0]['ipv6'] ?? null;
}
return dns_get_record($device['hostname'], DNS_A)[0]['ip'] ?? null;
} catch (\Exception $e) {
return null;
}
}
/**
* @param string $domain Domain which has to be parsed
* @param string $record DNS Record which should be searched

View File

@ -25,21 +25,32 @@
namespace LibreNMS\Util;
use Carbon\Carbon;
use LibreNMS\Config;
class Git
{
public static function repoPresent()
public static function repoPresent(): bool
{
$install_dir = Config::get('install_dir', realpath(__DIR__ . '/../..'));
return file_exists("$install_dir/.git");
}
public static function binaryExists()
public static function binaryExists(): bool
{
exec('git > /dev/null 2>&1', $response, $exit_code);
return $exit_code === 1;
}
public static function localCommit(): string
{
return rtrim(exec("git show --pretty='%H' -s HEAD"));
}
public static function localDate(): Carbon
{
return \Date::createFromTimestamp(exec("git show --pretty='%ct' -s HEAD"));
}
}

View File

@ -33,6 +33,7 @@ use LibreNMS\Config;
use LibreNMS\Data\Source\SnmpResponse;
use LibreNMS\Exceptions\FileNotFoundException;
use LibreNMS\Exceptions\InvalidModuleException;
use LibreNMS\Poller;
use Symfony\Component\Yaml\Yaml;
class ModuleTestHelper
@ -79,8 +80,6 @@ class ModuleTestHelper
*/
public function __construct($modules, $os, $variant = '')
{
global $influxdb;
$this->modules = self::resolveModuleDependencies((array) $modules);
$this->os = strtolower($os);
$this->variant = strtolower($variant);
@ -196,8 +195,10 @@ class ModuleTestHelper
$save_vdebug = Debug::isVerbose();
Debug::set();
Debug::setVerbose(false);
\Log::setDefaultDriver('console');
discover_device($device, $this->parseArgs('discovery'));
poll_device($device, $this->parseArgs('poller'));
$poller = app(Poller::class, ['device_spec' => $device_id, 'module_override' => $this->modules]);
$poller->poll();
Debug::set($save_debug);
Debug::setVerbose($save_vdebug);
$collection_output = ob_get_contents();
@ -533,6 +534,7 @@ class ModuleTestHelper
{
global $device;
Config::set('rrd.enable', false); // disable rrd
Config::set('rrdtool_version', '1.7.2'); // don't detect rrdtool version, rrdtool is not install on ci
if (! is_file($this->snmprec_file)) {
throw new FileNotFoundException("$this->snmprec_file does not exist!");
@ -592,7 +594,7 @@ class ModuleTestHelper
// Dump the discovered data
$data = array_merge_recursive($data, $this->dumpDb($device['device_id'], $discovered_modules, 'discovery'));
$device = device_by_id_cache($device_id, true); // refresh the device array
DeviceCache::get($device_id)->refresh(); // refresh the device
// Run the poller
if ($this->quiet) {
@ -601,7 +603,9 @@ class ModuleTestHelper
}
ob_start();
poll_device($device, $this->parseArgs('poller'));
\Log::setDefaultDriver('console');
$poller = app(Poller::class, ['device_spec' => $device_id, 'module_override' => $this->modules]);
$poller->poll();
$this->poller_output = ob_get_contents();
if ($this->quiet) {
@ -617,12 +621,12 @@ class ModuleTestHelper
$polled_modules = array_keys($this->poller_module_output);
// Dump polled data
$data = array_merge_recursive($data, $this->dumpDb($device['device_id'], $polled_modules, 'poller'));
$data = array_merge_recursive($data, $this->dumpDb($device_id, $polled_modules, 'poller'));
// Remove the test device, we don't need the debug from this
if ($device['hostname'] == $snmpsim->getIp()) {
Debug::set(false);
delete_device($device['device_id']);
delete_device($device_id);
}
if (! $no_save) {
@ -721,7 +725,7 @@ class ModuleTestHelper
// build joins
$join = '';
$select = ["`$table`.*"];
foreach ($info['joins'] ?: [] as $join_info) {
foreach ($info['joins'] ?? [] as $join_info) {
if (isset($join_info['custom'])) {
$join .= ' ' . $join_info['custom'];

View File

@ -1,6 +1,6 @@
<?php
/**
* Text.php
* StringHelpers.php
*
* -Description-
*
@ -19,7 +19,7 @@
*
* @link https://www.librenms.org
*
* @copyright 2018 Tony Murray
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/

View File

@ -25,6 +25,7 @@
namespace LibreNMS\Util;
use LibreNMS\Config;
use LibreNMS\DB\Eloquent;
use Symfony\Component\Process\Process;
@ -33,6 +34,9 @@ class Version
// Update this on release
const VERSION = '21.11.0';
/**
* @var bool
*/
protected $is_git_install = false;
public function __construct()
@ -40,12 +44,12 @@ class Version
$this->is_git_install = Git::repoPresent() && Git::binaryExists();
}
public static function get()
public static function get(): Version
{
return new static;
}
public function local()
public function local(): string
{
if ($this->is_git_install && $version = $this->fromGit()) {
return $version;
@ -54,7 +58,7 @@ class Version
return self::VERSION;
}
public function database()
public function database(): array
{
if (Eloquent::isConnected()) {
try {
@ -72,34 +76,52 @@ class Version
return ['last' => 'Not Connected', 'total' => 0];
}
private function fromGit()
private function fromGit(): string
{
return rtrim(shell_exec('git describe --tags 2>/dev/null'));
}
public function gitChangelog()
public function gitChangelog(): string
{
return $this->is_git_install
? rtrim(shell_exec('git log -10'))
: '';
}
public function gitDate()
public function gitDate(): string
{
return $this->is_git_install
? rtrim(shell_exec("git show --pretty='%ct' -s HEAD"))
: '';
}
public static function python()
public function python(): string
{
$proc = new Process(['python3', '--version']);
$proc->run();
if ($proc->getExitCode() !== 0) {
return null;
return '';
}
return explode(' ', rtrim($proc->getOutput()), 2)[1] ?? null;
return explode(' ', rtrim($proc->getOutput()), 2)[1] ?? '';
}
public function rrdtool(): string
{
$process = new Process([Config::get('rrdtool', 'rrdtool'), '--version']);
$process->run();
preg_match('/^RRDtool ([\w.]+) /', $process->getOutput(), $matches);
return str_replace('1.7.01.7.0', '1.7.0', $matches[1] ?? '');
}
public function netSnmp(): string
{
$process = new Process([Config::get('snmpget', 'snmpget'), '-V']);
$process->run();
preg_match('/[\w.]+$/', $process->getErrorOutput(), $matches);
return $matches[0] ?? '';
}
}

View File

@ -42,7 +42,7 @@ class Python extends BaseValidation
*/
public function validate(Validator $validator)
{
$version = Version::python();
$version = Version::get()->python();
if (empty($version)) {
$validator->fail('python3 not found', 'Install Python 3 for your system.');

41
app/Action.php Normal file
View File

@ -0,0 +1,41 @@
<?php
/*
* Action.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 App;
class Action
{
/**
* Execute an action and return the results
*
* @param string $action
* @param mixed ...$parameters
* @return mixed
*/
public static function execute(string $action, ...$parameters)
{
return app($action, $parameters)->execute();
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* RunAlertRulesAction.php
*
* Check alert rules for status changes
*
* 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 http://librenms.org
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Actions\Alerts;
use App\Models\Device;
use LibreNMS\Alert\AlertRules;
class RunAlertRulesAction
{
/**
* @var \LibreNMS\Alert\AlertRules
*/
private $rules;
/**
* @var \App\Models\Device
*/
private $device;
public function __construct(Device $device, AlertRules $rules)
{
$this->rules = $rules;
$this->device = $device;
}
public function execute(): void
{
// TODO inline logic
include_once base_path('includes/common.php');
include_once base_path('includes/dbFacile.php');
$this->rules->runRules($this->device->device_id);
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* UpdateDeviceGroupsAction.php
*
* Update device group associations by re-checking rules
*
* 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 http://librenms.org
*
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Actions\Device;
use App\Models\Device;
use App\Models\DeviceGroup;
use Log;
class UpdateDeviceGroupsAction
{
/**
* @var \App\Models\Device
*/
private $device;
public function __construct(Device $device)
{
$this->device = $device;
}
/**
* @return array[]
*/
public function execute(): array
{
if (! $this->device->exists) {
// Device not saved to DB, cowardly refusing
return [
'attached' => [],
'detached' => [],
'updated' => [],
];
}
$device_group_ids = DeviceGroup::query()
->with(['devices' => function ($query) {
$query->select('devices.device_id');
}])
->get()
->filter(function (DeviceGroup $device_group) {
if ($device_group->type == 'dynamic') {
try {
return $device_group->getParser()
->toQuery()
->where('devices.device_id', $this->device->device_id)
->exists();
} catch (\Illuminate\Database\QueryException $e) {
Log::error("Device Group '$device_group->name' generates invalid query: " . $e->getMessage());
return false;
}
}
// for static, if this device is include, keep it.
return $device_group->devices
->where('device_id', $this->device->device_id)
->isNotEmpty();
})->pluck('id');
return $this->device->groups()->sync($device_group_ids);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Console\Commands;
use App\Console\LnmsCommand;
use App\Polling\Measure\MeasurementManager;
use Illuminate\Database\QueryException;
use LibreNMS\Config;
use LibreNMS\Poller;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class DevicePoll extends LnmsCommand
{
protected $name = 'device:poll';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->addArgument('device spec', InputArgument::REQUIRED);
$this->addOption('modules', 'm', InputOption::VALUE_REQUIRED);
$this->addOption('no-data', 'x', InputOption::VALUE_NONE);
}
public function handle(MeasurementManager $measurements): int
{
$this->configureOutputOptions();
if ($this->option('no-data')) {
Config::set('rrd.enable', false);
Config::set('influxdb.enable', false);
Config::set('prometheus.enable', false);
Config::set('graphite.enable', false);
}
try {
$poller = app(Poller::class, ['device_spec' => $this->argument('device spec'), 'module_override' => explode(',', $this->option('modules'))]);
$polled = $poller->poll();
if ($polled > 0) {
if (! $this->output->isQuiet()) {
if ($polled > 1) {
$this->output->newLine();
$this->line(sprintf('Polled %d devices in %0.3fs', $polled, $measurements->getCategory('device')->getSummary('poll')->getDuration()));
}
$this->output->newLine();
$measurements->printStats();
}
return 0;
}
} catch (QueryException $e) {
if ($e->getCode() == 2002) {
$this->error(trans('commands.device:poll.errors.db_connect'));
return 1;
} elseif ($e->getCode() == 1045) {
// auth failed, don't need to include the query
$this->error(trans('commands.device:poll.errors.db_auth', ['error' => $e->getPrevious()->getMessage()]));
return 1;
}
$this->error($e->getMessage());
return 1;
}
return 1; // failed to poll
}
}

View File

@ -27,6 +27,7 @@ namespace App\Console;
use Illuminate\Console\Command;
use Illuminate\Validation\ValidationException;
use LibreNMS\Util\Debug;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Validator;
@ -45,7 +46,7 @@ abstract class LnmsCommand extends Command
$this->setDescription(__('commands.' . $this->getName() . '.description'));
}
public function isHidden()
public function isHidden(): bool
{
$env = $this->getLaravel() ? $this->getLaravel()->environment() : getenv('APP_ENV');
@ -125,4 +126,15 @@ abstract class LnmsCommand extends Command
exit(1);
}
}
protected function configureOutputOptions(): void
{
\Log::setDefaultDriver($this->getOutput()->isQuiet() ? 'stack' : 'console');
if (($verbosity = $this->getOutput()->getVerbosity()) >= 128) {
Debug::set();
if ($verbosity >= 256) {
Debug::setVerbose();
}
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Events;
use App\Models\Device;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DevicePolled
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @var \App\Models\Device
*/
public $device;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Device $device)
{
$this->device = $device;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Events;
use App\Models\Device;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PollingDevice
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @var \App\Models\Device
*/
public $device;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Device $device)
{
$this->device = $device;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -76,7 +76,7 @@ class AboutController extends Controller
'version_mysql' => Eloquent::version(),
'version_php' => phpversion(),
'version_laravel' => App::VERSION(),
'version_python' => Version::python(),
'version_python' => $version->python(),
'version_webserver' => $request->server('SERVER_SOFTWARE'),
'version_rrdtool' => Rrd::version(),
'version_netsnmp' => str_replace('version: ', '', rtrim(shell_exec(Config::get('snmpget', 'snmpget') . ' -V 2>&1'))),

View File

@ -0,0 +1,37 @@
<?php
namespace App\Listeners;
use App\Action;
use App\Actions\Alerts\RunAlertRulesAction;
use App\Events\DevicePolled;
use Log;
class CheckAlerts
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param DevicePolled $event
* @return void
*/
public function handle(DevicePolled $event)
{
Log::info('#### Start Alerts ####');
$start = microtime(true);
Action::execute(RunAlertRulesAction::class, $event->device);
$end = round(microtime(true) - $start, 4);
Log::info("#### End Alerts ({$end}s) ####\n");
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Listeners;
use App\Action;
use App\Actions\Device\UpdateDeviceGroupsAction;
use App\Events\DevicePolled;
use Log;
class UpdateDeviceGroups
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param DevicePolled $event
* @return void
*/
public function handle(DevicePolled $event)
{
Log::info('### Start Device Groups ###');
$dg_start = microtime(true);
// update device groups
$group_changes = Action::execute(UpdateDeviceGroupsAction::class, $event->device);
$added = implode(',', $group_changes['attached']);
$removed = implode(',', $group_changes['detached']);
$elapsed = round(microtime(true) - $dg_start, 4);
Log::debug("Groups Added: $added Removed: $removed");
Log::info("### End Device Groups ({$elapsed}s) ### \n");
}
}

View File

@ -27,7 +27,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use LibreNMS\Alerting\QueryBuilderFluentParser;
use Log;
use Permissions;
class DeviceGroup extends BaseModel
@ -70,53 +69,6 @@ class DeviceGroup extends BaseModel
}
}
/**
* Update the device groups for the given device or device_id
*
* @param Device|int $device
* @return array
*/
public static function updateGroupsFor($device)
{
$device = ($device instanceof Device ? $device : Device::find($device));
if (! $device instanceof Device) {
// could not load device
return [
'attached' => [],
'detached' => [],
'updated' => [],
];
}
$device_group_ids = static::query()
->with(['devices' => function ($query) {
$query->select('devices.device_id');
}])
->get()
->filter(function ($device_group) use ($device) {
/** @var DeviceGroup $device_group */
if ($device_group->type == 'dynamic') {
try {
return $device_group->getParser()
->toQuery()
->where('devices.device_id', $device->device_id)
->exists();
} catch (\Illuminate\Database\QueryException $e) {
Log::error("Device Group '$device_group->name' generates invalid query: " . $e->getMessage());
return false;
}
}
// for static, if this device is include, keep it.
return $device_group->devices
->where('device_id', $device->device_id)
->isNotEmpty();
})->pluck('id');
return $device->groups()->sync($device_group_ids);
}
/**
* Get a query builder parser instance from this device group
*

View File

@ -39,7 +39,7 @@ class DeviceObserver
}
// key attribute changes
foreach (['os', 'sysName', 'version', 'hardware', 'features', 'serial', 'icon', 'type'] as $attribute) {
foreach (['os', 'sysName', 'version', 'hardware', 'features', 'serial', 'icon', 'type', 'ip'] as $attribute) {
if ($device->isDirty($attribute)) {
Log::event(self::attributeChangedMessage($attribute, $device->$attribute, $device->getOriginal($attribute)), $device, 'system', 3);
}

View File

@ -27,6 +27,8 @@ namespace App\Polling\Measure;
use DB;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Collection;
use Log;
class MeasurementManager
{
@ -36,20 +38,16 @@ class MeasurementManager
const NO_COLOR = "\e[0m";
/**
* @var MeasurementCollection
* @var \Illuminate\Support\Collection<MeasurementCollection>
*/
private static $snmp;
/**
* @var MeasurementCollection
*/
private static $db;
private static $categories;
public function __construct()
{
if (self::$snmp === null) {
self::$snmp = new MeasurementCollection();
self::$db = new MeasurementCollection();
if (self::$categories === null) {
self::$categories = new Collection;
self::$categories->put('snmp', new MeasurementCollection());
self::$categories->put('db', new MeasurementCollection());
}
}
@ -64,12 +62,20 @@ class MeasurementManager
});
}
/**
* Update statistics for the given category
*/
public function record(string $category, Measurement $measurement): void
{
$this->getCategory($category)->record($measurement);
}
/**
* Update statistics for db operations
*/
public function recordDb(Measurement $measurement): void
{
self::$db->record($measurement);
$this->record('db', $measurement);
}
/**
@ -77,25 +83,24 @@ class MeasurementManager
*/
public function printChangedStats(): void
{
printf(
'>> %sSNMP%s: [%d/%.2fs] %sMySQL%s: [%d/%.2fs]',
self::SNMP_COLOR,
self::NO_COLOR,
self::$snmp->getCountDiff(),
self::$snmp->getDurationDiff(),
self::DB_COLOR,
self::NO_COLOR,
self::$db->getCountDiff(),
self::$db->getDurationDiff()
);
app('Datastore')->getStats()->each(function (MeasurementCollection $stats, $datastore) {
printf(' %s%s%s: [%d/%.2fs]', self::DATASTORE_COLOR, $datastore, self::NO_COLOR, $stats->getCountDiff(), $stats->getDurationDiff());
$dsStats = app('Datastore')->getStats()->map(function (MeasurementCollection $stats, $datastore) {
return sprintf('%s%s%s: [%d/%.2fs]', self::DATASTORE_COLOR, $datastore, self::NO_COLOR, $stats->getCountDiff(), $stats->getDurationDiff());
});
$this->checkpoint();
Log::info(sprintf(
'>> %sSNMP%s: [%d/%.2fs] %sMySQL%s: [%d/%.2fs] %s',
self::SNMP_COLOR,
self::NO_COLOR,
$this->getCategory('snmp')->getCountDiff(),
$this->getCategory('snmp')->getDurationDiff(),
self::DB_COLOR,
self::NO_COLOR,
$this->getCategory('db')->getCountDiff(),
$this->getCategory('db')->getDurationDiff(),
$dsStats->implode(' ')
));
echo PHP_EOL;
$this->checkpoint();
}
/**
@ -103,8 +108,7 @@ class MeasurementManager
*/
public function checkpoint(): void
{
self::$snmp->checkpoint();
self::$db->checkpoint();
self::$categories->each->checkpoint();
app('Datastore')->getStats()->each->checkpoint();
}
@ -113,7 +117,7 @@ class MeasurementManager
*/
public function recordSnmp(Measurement $measurement): void
{
self::$snmp->record($measurement);
$this->record('snmp', $measurement);
}
/**
@ -121,22 +125,36 @@ class MeasurementManager
*/
public function printStats(): void
{
$this->printSummary('SNMP', self::$snmp, self::SNMP_COLOR);
$this->printSummary('SQL', self::$db, self::DB_COLOR);
$this->printSummary('SNMP', $this->getCategory('snmp'), self::SNMP_COLOR);
$this->printSummary('SQL', $this->getCategory('db'), self::DB_COLOR);
app('Datastore')->getStats()->each(function (MeasurementCollection $stats, string $datastore) {
$this->printSummary($datastore, $stats, self::DATASTORE_COLOR);
});
}
private function printSummary(string $name, MeasurementCollection $collection, string $color = ''): void
public function getCategory(string $category): MeasurementCollection
{
printf('%s%s%s [%d/%.2fs]:', $color, $name, $color ? self::NO_COLOR : '', $collection->getTotalCount(), $collection->getTotalDuration());
if (! self::$categories->has($category)) {
self::$categories->put($category, new MeasurementCollection());
}
$collection->each(function (MeasurementSummary $stat) {
printf(' %s[%d/%.2fs]', ucfirst($stat->getType()), $stat->getCount(), $stat->getDuration());
return self::$categories->get($category);
}
public function printSummary(string $name, MeasurementCollection $collection, string $color = ''): void
{
$summaries = $collection->map(function (MeasurementSummary $stat) {
return sprintf('%s[%d/%.2fs]', ucfirst($stat->getType()), $stat->getCount(), $stat->getDuration());
});
echo PHP_EOL;
Log::info(sprintf('%s%s%s [%d/%.2fs]: %s',
$color,
$name,
$color ? self::NO_COLOR : '',
$collection->getTotalCount(),
$collection->getTotalDuration(),
$summaries->implode(' ')
));
}
}

View File

@ -32,6 +32,13 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton('device-cache', function ($app) {
return new \LibreNMS\Cache\Device();
});
$this->app->bind(\App\Models\Device::class, function () {
/** @var \LibreNMS\Cache\Device $cache */
$cache = $this->app->make('device-cache');
return $cache->hasPrimary() ? $cache->getPrimary() : new \App\Models\Device;
});
}
/**

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Listeners\MarkNotificationsRead;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
@ -15,7 +14,15 @@ class EventServiceProvider extends ServiceProvider
protected $listen = [
\Illuminate\Auth\Events\Login::class => ['App\Listeners\AuthEventListener@login'],
\Illuminate\Auth\Events\Logout::class => ['App\Listeners\AuthEventListener@logout'],
\App\Events\UserCreated::class => [MarkNotificationsRead::class],
\App\Events\UserCreated::class => [
\App\Listeners\MarkNotificationsRead::class,
],
\App\Events\PollingDevice::class => [
],
\App\Events\DevicePolled::class => [
\App\Listeners\CheckAlerts::class,
\App\Listeners\UpdateDeviceGroups::class,
],
];
/**

View File

@ -55,6 +55,12 @@ return [
'ignore_exceptions' => false,
],
'console_debug' => [
'driver' => 'stack',
'channels' => ['single', 'stdout_debug'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => env('APP_LOG', \LibreNMS\Config::get('log_file', base_path('logs/librenms.log'))),
@ -96,7 +102,7 @@ return [
'level' => 'debug',
],
'stdout' => [
'stdout_debug' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => \LibreNMS\Util\CliColorFormatter::class,
@ -106,6 +112,16 @@ return [
'level' => 'debug',
],
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => \LibreNMS\Util\CliColorFormatter::class,
'with' => [
'stream' => 'php://output',
],
'level' => 'info',
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),

View File

@ -15,6 +15,7 @@ $init_modules = ['discovery'];
require __DIR__ . '/includes/init.php';
$start = microtime(true);
Log::setDefaultDriver('console');
$sqlparams = [];
$options = getopt('h:m:i:n:d::v::a::q', ['os:', 'type:']);

View File

@ -623,14 +623,10 @@ function version_info($remote = false)
}
$output['db_schema'] = vsprintf('%s (%s)', $version->database());
$output['php_ver'] = phpversion();
$output['python_ver'] = \LibreNMS\Util\Version::python();
$output['python_ver'] = $version->python();
$output['mysql_ver'] = \LibreNMS\DB\Eloquent::isConnected() ? \LibreNMS\DB\Eloquent::version() : '?';
$output['rrdtool_ver'] = str_replace('1.7.01.7.0', '1.7.0', implode(' ', array_slice(explode(' ', shell_exec(
Config::get('rrdtool', 'rrdtool') . ' --version |head -n1'
)), 1, 1)));
$output['netsnmp_ver'] = str_replace('version: ', '', rtrim(shell_exec(
Config::get('snmpget', 'snmpget') . ' -V 2>&1'
)));
$output['rrdtool_ver'] = $version->rrdtool();
$output['netsnmp_ver'] = $version->netSnmp();
return $output;
}//end version_info()

View File

@ -1,3 +1,3 @@
<?php
(new \LibreNMS\Modules\OS())->discover($os);
(new \LibreNMS\Modules\Os())->discover($os);

View File

@ -74,7 +74,7 @@ function parse_modules($type, $options)
{
$override = false;
if ($options['m']) {
if (! empty($options['m'])) {
Config::set("{$type}_modules", []);
foreach (explode(',', $options['m']) as $module) {
// parse submodules (only supported by some modules)

View File

@ -5,4 +5,4 @@ use LibreNMS\OS;
if (! $os instanceof OS) {
$os = OS::make($device);
}
(new \LibreNMS\Modules\OS())->poll($os);
(new \LibreNMS\Modules\Os())->poll($os);

View File

@ -3665,60 +3665,30 @@ parameters:
count: 1
path: LibreNMS/Modules/Nac.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:cleanup\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:discover\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:handleChanges\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:poll\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:sysContact\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Method LibreNMS\\\\Modules\\\\OS\\:\\:updateLocation\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
-
message: "#^Variable \\$features on left side of \\?\\? is never defined\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
path: LibreNMS/Modules/Os.php
-
message: "#^Variable \\$hardware on left side of \\?\\? is never defined\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
path: LibreNMS/Modules/Os.php
-
message: "#^Variable \\$location in empty\\(\\) always exists and is always falsy\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
path: LibreNMS/Modules/Os.php
-
message: "#^Variable \\$serial on left side of \\?\\? is never defined\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
path: LibreNMS/Modules/Os.php
-
message: "#^Variable \\$version on left side of \\?\\? is never defined\\.$#"
count: 1
path: LibreNMS/Modules/OS.php
path: LibreNMS/Modules/Os.php
-
message: "#^Method LibreNMS\\\\Modules\\\\PrinterSupplies\\:\\:cleanup\\(\\) has no return type specified\\.$#"
@ -3915,11 +3885,6 @@ parameters:
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:persistGraphs\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:preCache\\(\\) has no return type specified\\.$#"
count: 1
@ -5440,36 +5405,6 @@ parameters:
count: 1
path: LibreNMS/Util/Colors.php
-
message: "#^Method LibreNMS\\\\Util\\\\Debug\\:\\:disableCliDebugOutput\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Method LibreNMS\\\\Util\\\\Debug\\:\\:disableQueryDebug\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Method LibreNMS\\\\Util\\\\Debug\\:\\:enableCliDebugOutput\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Method LibreNMS\\\\Util\\\\Debug\\:\\:enableQueryDebug\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Property LibreNMS\\\\Util\\\\Debug\\:\\:\\$debug has no type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Property LibreNMS\\\\Util\\\\Debug\\:\\:\\$verbose has no type specified\\.$#"
count: 1
path: LibreNMS/Util/Debug.php
-
message: "#^Property LibreNMS\\\\Util\\\\Dns\\:\\:\\$resolver has no type specified\\.$#"
count: 1
@ -5765,16 +5700,6 @@ parameters:
count: 1
path: LibreNMS/Util/FileCategorizer.php
-
message: "#^Method LibreNMS\\\\Util\\\\Git\\:\\:binaryExists\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Git.php
-
message: "#^Method LibreNMS\\\\Util\\\\Git\\:\\:repoPresent\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Git.php
-
message: "#^Method LibreNMS\\\\Util\\\\GitHub\\:\\:__construct\\(\\) has parameter \\$file with no type specified\\.$#"
count: 1
@ -6845,46 +6770,6 @@ parameters:
count: 1
path: LibreNMS/Util/Validate.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:database\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:fromGit\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:get\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:gitChangelog\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:gitDate\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:local\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\Util\\\\Version\\:\\:python\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Property LibreNMS\\\\Util\\\\Version\\:\\:\\$is_git_install has no type specified\\.$#"
count: 1
path: LibreNMS/Util/Version.php
-
message: "#^Method LibreNMS\\\\ValidationResult\\:\\:consolePrint\\(\\) has no return type specified\\.$#"
count: 1

View File

@ -24,6 +24,8 @@
* @author Heath Barnhart <hbarnhart@kanren.net>
*/
use App\Action;
use App\Actions\Device\UpdateDeviceGroupsAction;
use LibreNMS\Alert\AlertRules;
use LibreNMS\Config;
use LibreNMS\Data\Store\Datastore;
@ -33,6 +35,7 @@ $init_modules = ['polling', 'alerts', 'laravel'];
require __DIR__ . '/includes/init.php';
$poller_start = microtime(true);
Log::setDefaultDriver('console');
echo Config::get('project_name') . " Poller\n";
$options = getopt('h:m:i:n:r::d::v::a::f::q');
@ -139,7 +142,7 @@ foreach (dbFetch($query) as $device) {
// Update device_groups
echo "### Start Device Groups ###\n";
$dg_start = microtime(true);
$group_changes = \App\Models\DeviceGroup::updateGroupsFor($device['device_id']);
$group_changes = Action::execute(UpdateDeviceGroupsAction::class);
d_echo('Groups Added: ' . implode(',', $group_changes['attached']) . PHP_EOL);
d_echo('Groups Removed: ' . implode(',', $group_changes['detached']) . PHP_EOL);
echo '### End Device Groups, runtime: ' . round(microtime(true) - $dg_start, 4) . "s ### \n\n";

View File

@ -69,6 +69,20 @@ return [
'device spec' => 'Device to ping one of: <Device ID>, <Hostname/IP>, all',
],
],
'device:poll' => [
'description' => 'Poll data from device(s) as defined by discovery',
'arguments' => [
'device spec' => 'Device spec to poll: device_id, hostname, wildcard, odd, even, all',
],
'options' => [
'modules' => 'Specify single module to be run. Comma separate modules, submodules may be added with /',
'no-data' => 'Do not update datastores (RRD, InfluxDB, etc)',
],
'errors' => [
'db_connect' => 'Failed to connect to database. Verify database service is running and connection settings.',
'db_auth' => 'Failed to connect to database. Verify credentials: :error',
],
],
'key:rotate' => [
'description' => 'Rotate APP_KEY, this decrypts all encrypted data with the given old key and stores it with the new key in APP_KEY.',
'arguments' => [

View File

@ -111,8 +111,7 @@ Artisan::command('device:add
if (($verbosity = $this->getOutput()->getVerbosity()) >= 128) {
Debug::set();
if ($verbosity >= 256) {
global $verbose;
$verbose = true;
Debug::setVerbose();
}
}

View File

@ -100,10 +100,8 @@ class OSModulesTest extends DBTestCase
$filename = $helper->getJsonFilepath(true);
$expected_data = $helper->getTestData();
$results = $helper->generateTestData($this->getSnmpsim(), true);
} catch (FileNotFoundException $e) {
return $this->fail($e->getMessage());
} catch (InvalidModuleException $e) {
return $this->fail($e->getMessage());
} catch (FileNotFoundException|InvalidModuleException $e) {
$this->fail($e->getMessage());
}
if (is_null($results)) {
@ -168,8 +166,10 @@ class OSModulesTest extends DBTestCase
private function stubClasses(): void
{
$this->app->bind('log', function ($app) {
return \Mockery::mock('\App\Facades\LogManager[event]', [$app])
->shouldReceive('event');
$mock = \Mockery::mock('\App\Facades\LogManager[event]', [$app]);
$mock->shouldReceive('event');
return $mock;
});
$this->app->bind(Fping::class, function ($app) {