mirror of
				https://github.com/librenms/librenms.git
				synced 2024-10-07 16:52:45 +00:00 
			
		
		
		
	* Remove Log::event Use the Eventlog class directly instead * wip * wip * wip * Apply fixes from StyleCI * Update Eventlog.php
		
			
				
	
	
		
			369 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?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 Illuminate\Database\Eloquent\Builder;
 | 
						|
use Illuminate\Support\Str;
 | 
						|
use LibreNMS\Enum\Alert;
 | 
						|
use LibreNMS\Exceptions\PollerException;
 | 
						|
use LibreNMS\Polling\ConnectivityHelper;
 | 
						|
use LibreNMS\RRD\RrdDefinition;
 | 
						|
use LibreNMS\Util\Debug;
 | 
						|
use LibreNMS\Util\Dns;
 | 
						|
use LibreNMS\Util\Module;
 | 
						|
use LibreNMS\Util\StringHelpers;
 | 
						|
use LibreNMS\Util\Version;
 | 
						|
use Psr\Log\LoggerInterface;
 | 
						|
use Throwable;
 | 
						|
 | 
						|
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() && ! defined('PHPUNIT_RUNNING')) {
 | 
						|
            \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()));
 | 
						|
            \Log::channel('single')->alert(sprintf('INFO: device:poll %s (%s) polled in %0.3fs',
 | 
						|
                $this->device->hostname,
 | 
						|
                $this->device->device_id,
 | 
						|
                $measurement->getDuration()));
 | 
						|
 | 
						|
            // check if the poll took too long and log an event
 | 
						|
            if ($measurement->getDuration() > Config::get('rrd.step')) {
 | 
						|
                \App\Models\Eventlog::log('Polling took longer than ' . round(Config::get('rrd.step') / 60, 2) .
 | 
						|
                    ' minutes!  This will cause gaps in graphs.', $this->device, 'system', 5);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $polled;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get the total number of devices to poll.
 | 
						|
     */
 | 
						|
    public function totalDevices(): int
 | 
						|
    {
 | 
						|
        return $this->buildDeviceQuery()->count();
 | 
						|
    }
 | 
						|
 | 
						|
    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 {
 | 
						|
                    $instance = Module::fromName($module);
 | 
						|
                    $instance->poll($this->os);
 | 
						|
                } catch (Throwable $e) {
 | 
						|
                    // isolate module exceptions so they don't disrupt the polling process
 | 
						|
                    $this->logger->error("%rError polling $module module for {$this->device->hostname}.%n $e", ['color' => true]);
 | 
						|
                    \App\Models\Eventlog::log("Error polling $module module. Check log file for more details.", $this->device, 'poller', Alert::ERROR);
 | 
						|
                    report($e);
 | 
						|
                }
 | 
						|
 | 
						|
                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()) {
 | 
						|
            $this->logger->info(Version::get()->header());
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |