Convert Config to a singleton (#16349)

* Convert Config to a singleton
Continuation of #14364 by @Jellyfrog
This time, make the old class a shim for the facade.  Will update references in a separate PR.

* Remove logging config call

* Apply fixes from StyleCI

* Fix bad Git constructor call

* Fail on config table does not exist instead of throw exception

* Inline LibrenmsConfig::isRegistered()

* Debug call in case there are more issues,
remove before merge.

* Fix up config tests

* Allow config cache controlled by CONFIG_CACHE_TTL (disabled by default for now)

* Enable config cache for tests

* Remove debug statement and deprecation phpdoc

* Apply fixes from StyleCI

---------

Co-authored-by: Tony Murray <murrant@users.noreply.github.com>
This commit is contained in:
Tony Murray 2024-09-09 11:48:07 -05:00 committed by GitHub
parent 64241dbdf3
commit f1e7a218f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 713 additions and 470 deletions

View File

@ -25,119 +25,31 @@
namespace LibreNMS;
use App\Models\Callback;
use App\Models\GraphType;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use LibreNMS\Data\Store\Rrd;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Version;
use Log;
use App\Facades\LibrenmsConfig;
// not yet: @deprecated Please use the facade App\Facades\LibrenmsConfig instead
class Config
{
private static $config;
/**
* Load the config, if the database connected, pull in database settings.
*
* return &array
*/
public static function load()
{
// don't reload the config if it is already loaded, reload() should be used for that
if (self::isLoaded()) {
return self::$config;
}
// merge all config sources together config_definitions.json > db config > config.php
self::loadDefaults();
self::loadDB();
self::loadUserConfigFile(self::$config);
// final cleanups and validations
self::processConfig();
// set to global for legacy/external things (is this needed?)
global $config;
$config = self::$config;
return self::$config;
}
/**
* Reload the config from files/db
*
* @return mixed
*/
public static function reload()
{
self::$config = null;
return self::load();
}
/**
* Get the config setting definitions
*
* @return array
*/
public static function getDefinitions()
public static function getDefinitions(): array
{
return json_decode(file_get_contents(base_path('misc/config_definitions.json')), true)['config'];
}
private static function loadDefaults()
{
self::$config['install_dir'] = base_path();
$definitions = self::getDefinitions();
foreach ($definitions as $path => $def) {
if (array_key_exists('default', $def)) {
Arr::set(self::$config, $path, $def['default']);
}
}
// load macros from json
$macros = json_decode(file_get_contents(base_path('misc/macros.json')), true);
Arr::set(self::$config, 'alert.macros.rule', $macros);
self::processDefaults();
return LibrenmsConfig::getDefinitions();
}
/**
* Load the user config from config.php
*
* @param array $config (this should be self::$config)
*/
private static function loadUserConfigFile(&$config)
{
// Load user config file
if (is_file(base_path('config.php'))) {
@include base_path('config.php');
}
}
/**
* Get a config value, if non existent null (or default if set) will be returned
* Get a config value, if non-existent null (or default if set) will be returned
*
* @param string $key period separated config variable name
* @param mixed $default optional value to return if the setting is not set
* @return mixed
*/
public static function get($key, $default = null)
public static function get($key, $default = null): mixed
{
if (isset(self::$config[$key])) {
return self::$config[$key];
}
if (! Str::contains($key, '.')) {
return $default;
}
return Arr::get(self::$config, $key, $default);
return LibrenmsConfig::get($key, $default);
}
/**
@ -146,9 +58,9 @@ class Config
*
* @param string|array $key
*/
public static function forget($key)
public static function forget($key): void
{
Arr::forget(self::$config, $key);
LibrenmsConfig::forget($key);
}
/**
@ -162,17 +74,9 @@ class Config
* @param mixed $default will be returned if the setting is not set on the device or globally
* @return mixed
*/
public static function getDeviceSetting($device, $key, $global_prefix = null, $default = null)
public static function getDeviceSetting($device, $key, $global_prefix = null, $default = null): mixed
{
if (isset($device[$key])) {
return $device[$key];
}
if (isset($global_prefix)) {
$key = "$global_prefix.$key";
}
return self::get($key, $default);
return LibrenmsConfig::getDeviceSetting($device, $key, $global_prefix, $default);
}
/**
@ -183,22 +87,9 @@ class Config
* @param mixed $default optional value to return if the setting is not set
* @return mixed
*/
public static function getOsSetting($os, $key, $default = null)
public static function getOsSetting($os, $key, $default = null): mixed
{
if ($os) {
\LibreNMS\Util\OS::loadDefinition($os);
if (isset(self::$config['os'][$os][$key])) {
return self::$config['os'][$os][$key];
}
$os_key = "os.$os.$key";
if (self::has($os_key)) {
return self::get($os_key);
}
}
return $default;
return LibrenmsConfig::getOsSetting($os, $key, $default);
}
/**
@ -214,25 +105,7 @@ class Config
*/
public static function getCombined(?string $os, string $key, string $global_prefix = '', array $default = []): array
{
$global_key = $global_prefix . $key;
if (! isset(self::$config['os'][$os][$key])) {
if (! Str::contains($global_key, '.')) {
return (array) self::get($global_key, $default);
}
if (! self::has("os.$os.$key")) {
return (array) self::get($global_key, $default);
}
}
if (! self::has("os.$os.$key")) {
return (array) self::get($global_key, $default);
}
return array_unique(array_merge(
(array) self::get($global_key),
(array) self::getOsSetting($os, $key)
));
return LibrenmsConfig::getCombined($os, $key, $global_prefix, $default);
}
/**
@ -241,9 +114,9 @@ class Config
* @param mixed $key period separated config variable name
* @param mixed $value
*/
public static function set($key, $value)
public static function set($key, $value): void
{
Arr::set(self::$config, $key, $value);
LibrenmsConfig::set($key, $value);
}
/**
@ -253,29 +126,9 @@ class Config
* @param mixed $value
* @return bool if the save was successful
*/
public static function persist($key, $value)
public static function persist($key, $value): bool
{
try {
Arr::set(self::$config, $key, $value);
\App\Models\Config::updateOrCreate(['config_name' => $key], [
'config_name' => $key,
'config_value' => $value,
]);
// delete any children (there should not be any unless it is legacy)
\App\Models\Config::query()->where('config_name', 'like', "$key.%")->delete();
return true;
} catch (Exception $e) {
if (class_exists(Log::class)) {
Log::error($e);
}
if (Debug::isEnabled()) {
echo $e;
}
return false;
}
return LibrenmsConfig::persist($key, $value);
}
/**
@ -285,14 +138,9 @@ class Config
* @param string $key
* @return int|false
*/
public static function erase($key)
public static function erase($key): bool|int
{
self::forget($key);
try {
return \App\Models\Config::withChildren($key)->delete();
} catch (Exception $e) {
return false;
}
return LibrenmsConfig::erase($key);
}
/**
@ -301,17 +149,9 @@ class Config
* @param string $key period separated config variable name
* @return bool
*/
public static function has($key)
public static function has($key): bool
{
if (isset(self::$config[$key])) {
return true;
}
if (! Str::contains($key, '.')) {
return false;
}
return Arr::has(self::$config, $key);
return LibrenmsConfig::has($key);
}
/**
@ -319,9 +159,9 @@ class Config
*
* @return string
*/
public static function toJson()
public static function toJson(): string
{
return json_encode(self::$config);
return LibrenmsConfig::toJson();
}
/**
@ -329,193 +169,9 @@ class Config
*
* @return array
*/
public static function getAll()
public static function getAll(): array
{
return self::$config;
}
/**
* merge the database config with the global config
* Global config overrides db
*/
private static function loadDB()
{
try {
\App\Models\Config::get(['config_name', 'config_value'])
->each(function ($item) {
Arr::set(self::$config, $item->config_name, $item->config_value);
});
} catch (QueryException $e) {
// possibly table config doesn't exist yet
}
// load graph types from the database
self::loadGraphsFromDb(self::$config);
}
private static function loadGraphsFromDb(&$config)
{
try {
$graph_types = GraphType::all()->toArray();
} catch (QueryException $e) {
// possibly table config doesn't exist yet
$graph_types = [];
}
// load graph types from the database
foreach ($graph_types as $graph) {
$g = [];
foreach ($graph as $k => $v) {
if (strpos($k, 'graph_') == 0) {
// remove leading 'graph_' from column name
$key = str_replace('graph_', '', $k);
} else {
$key = $k;
}
$g[$key] = $v;
}
$config['graph_types'][$g['type']][$g['subtype']] = $g;
}
}
/**
* Handle defaults that are set programmatically
*/
private static function processDefaults()
{
Arr::set(self::$config, 'log_dir', base_path('logs'));
Arr::set(self::$config, 'distributed_poller_name', php_uname('n'));
// set base_url from access URL
if (isset($_SERVER['SERVER_NAME']) && isset($_SERVER['SERVER_PORT'])) {
$port = $_SERVER['SERVER_PORT'] != 80 ? ':' . $_SERVER['SERVER_PORT'] : '';
// handle literal IPv6
$server = Str::contains($_SERVER['SERVER_NAME'], ':') ? "[{$_SERVER['SERVER_NAME']}]" : $_SERVER['SERVER_NAME'];
Arr::set(self::$config, 'base_url', "http://$server$port/");
}
// graph color copying
Arr::set(self::$config, 'graph_colours.mega', array_merge(
(array) Arr::get(self::$config, 'graph_colours.psychedelic', []),
(array) Arr::get(self::$config, 'graph_colours.manycolours', []),
(array) Arr::get(self::$config, 'graph_colours.default', []),
(array) Arr::get(self::$config, 'graph_colours.mixed', [])
));
}
/**
* Process the config after it has been loaded.
* Make sure certain variables have been set properly and
*/
private static function processConfig()
{
// If we're on SSL, let's properly detect it
if (
isset($_SERVER['HTTPS']) ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
) {
self::set('base_url', preg_replace('/^http:/', 'https:', self::get('base_url', '')));
}
self::set('base_url', Str::finish(self::get('base_url', ''), '/'));
if (! self::get('email_from')) {
self::set('email_from', '"' . self::get('project_name') . '" <' . self::get('email_user') . '@' . php_uname('n') . '>');
}
// Define some variables if they aren't set by user definition in config_definitions.json
self::set('applied_site_style', self::get('site_style'));
self::setDefault('html_dir', '%s/html', ['install_dir']);
self::setDefault('rrd_dir', '%s/rrd', ['install_dir']);
self::setDefault('mib_dir', '%s/mibs', ['install_dir']);
self::setDefault('log_dir', '%s/logs', ['install_dir']);
self::setDefault('log_file', '%s/%s.log', ['log_dir', 'project_id']);
self::setDefault('plugin_dir', '%s/plugins', ['html_dir']);
self::setDefault('temp_dir', sys_get_temp_dir() ?: '/tmp');
self::setDefault('irc_nick', '%s', ['project_name']);
self::setDefault('irc_chan.0', '##%s', ['project_id']);
self::setDefault('page_title_suffix', '%s', ['project_name']);
// self::setDefault('email_from', '"%s" <%s@' . php_uname('n') . '>', ['project_name', 'email_user']); // FIXME email_from set because alerting config
// deprecated variables
self::deprecatedVariable('rrdgraph_real_95th', 'rrdgraph_real_percentile');
self::deprecatedVariable('fping_options.millisec', 'fping_options.interval');
self::deprecatedVariable('discovery_modules.cisco-vrf', 'discovery_modules.vrf');
self::deprecatedVariable('discovery_modules.toner', 'discovery_modules.printer-supplies');
self::deprecatedVariable('poller_modules.toner', 'poller_modules.printer-supplies');
self::deprecatedVariable('discovery_modules.cisco-sla', 'discovery_modules.slas');
self::deprecatedVariable('poller_modules.cisco-sla', 'poller_modules.slas');
self::deprecatedVariable('oxidized.group', 'oxidized.maps.group');
// migrate device display
if (! self::has('device_display_default')) {
$display_value = '{{ $hostname }}';
if (self::get('force_hostname_to_sysname')) {
$display_value = '{{ $sysName }}';
} elseif (self::get('force_ip_to_sysname')) {
$display_value = '{{ $sysName_fallback }}';
}
self::persist('device_display_default', $display_value);
}
// make sure we have full path to binaries in case PATH isn't set
foreach (['fping', 'fping6', 'snmpgetnext', 'rrdtool', 'traceroute'] as $bin) {
if (! is_executable(self::get($bin))) {
self::persist($bin, self::locateBinary($bin));
}
}
if (! self::has('rrdtool_version')) {
self::persist('rrdtool_version', Rrd::version());
}
if (! self::has('snmp.unescape')) {
self::persist('snmp.unescape', version_compare(Version::get()->netSnmp(), '5.8.0', '<'));
}
if (! self::has('reporting.usage')) {
self::persist('reporting.usage', (bool) Callback::get('enabled'));
}
self::populateTime();
// populate legacy DB credentials, just in case something external uses them. Maybe remove this later
self::populateLegacyDbCredentials();
}
/**
* Set default values for defaults that depend on other settings, if they are not already loaded
*
* @param string $key
* @param string $value value to set to key or vsprintf() format string for values below
* @param array $format_values array of keys to send to vsprintf()
*/
private static function setDefault($key, $value, $format_values = [])
{
if (! self::has($key)) {
if (is_string($value)) {
$format_values = array_map('\LibreNMS\Config::get', $format_values);
self::set($key, vsprintf($value, $format_values));
} else {
self::set($key, $value);
}
}
}
/**
* Copy data from old variables to new ones.
*
* @param string $old
* @param string $new
*/
private static function deprecatedVariable($old, $new)
{
if (self::has($old)) {
if (Debug::isEnabled()) {
echo "Copied deprecated config $old to $new\n";
}
self::set($new, self::get($old));
}
return LibrenmsConfig::getAll();
}
/**
@ -524,62 +180,13 @@ class Config
* @param string $binary
* @return mixed
*/
public static function locateBinary($binary)
public static function locateBinary($binary): mixed
{
if (! Str::contains($binary, '/')) {
$output = `whereis -b $binary`;
$list = trim(substr($output, strpos($output, ':') + 1));
$targets = explode(' ', $list);
foreach ($targets as $target) {
if (is_executable($target)) {
return $target;
}
}
}
return $binary;
return LibrenmsConfig::locateBinary($binary);
}
private static function populateTime()
public static function populateLegacyDbCredentials(): void
{
$now = time();
$now -= $now % 300;
self::set('time.now', $now);
self::set('time.onehour', $now - 3600); // time() - (1 * 60 * 60);
self::set('time.fourhour', $now - 14400); // time() - (4 * 60 * 60);
self::set('time.sixhour', $now - 21600); // time() - (6 * 60 * 60);
self::set('time.twelvehour', $now - 43200); // time() - (12 * 60 * 60);
self::set('time.day', $now - 86400); // time() - (24 * 60 * 60);
self::set('time.twoday', $now - 172800); // time() - (2 * 24 * 60 * 60);
self::set('time.week', $now - 604800); // time() - (7 * 24 * 60 * 60);
self::set('time.twoweek', $now - 1209600); // time() - (2 * 7 * 24 * 60 * 60);
self::set('time.month', $now - 2678400); // time() - (31 * 24 * 60 * 60);
self::set('time.twomonth', $now - 5356800); // time() - (2 * 31 * 24 * 60 * 60);
self::set('time.threemonth', $now - 8035200); // time() - (3 * 31 * 24 * 60 * 60);
self::set('time.sixmonth', $now - 16070400); // time() - (6 * 31 * 24 * 60 * 60);
self::set('time.year', $now - 31536000); // time() - (365 * 24 * 60 * 60);
self::set('time.twoyear', $now - 63072000); // time() - (2 * 365 * 24 * 60 * 60);
}
public static function populateLegacyDbCredentials()
{
$db = config('database.default');
self::set('db_host', config("database.connections.$db.host", 'localhost'));
self::set('db_name', config("database.connections.$db.database", 'librenms'));
self::set('db_user', config("database.connections.$db.username", 'librenms'));
self::set('db_pass', config("database.connections.$db.password"));
self::set('db_port', config("database.connections.$db.port", 3306));
self::set('db_socket', config("database.connections.$db.unix_socket"));
}
/**
* Check if the config has been loaded yet
*
* @return bool
*/
public static function isLoaded(): bool
{
return ! is_null(self::$config);
LibrenmsConfig::populateLegacyDbCredentials();
}
}

View File

@ -20,6 +20,7 @@
namespace LibreNMS;
use App\Facades\LibrenmsConfig;
use App\Models\Device;
use App\Models\Eventlog;
use App\Models\Port;
@ -728,9 +729,9 @@ class IRCBot
return $this->loadExternal();
}
$new_config = Config::load();
LibrenmsConfig::reload();
$this->respond('Reloading configuration & defaults');
if ($new_config != $this->config) {
if (LibrenmsConfig::getAll() != $this->config) {
$this->__construct();
return;

View File

@ -25,10 +25,10 @@
namespace LibreNMS\Util;
use App\Facades\LibrenmsConfig;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Traits\RuntimeClassCache;
use Symfony\Component\Process\Process;
@ -42,7 +42,7 @@ class Git
public function __construct(int $cache = 0)
{
$this->runtimeCacheExternalTTL = $cache;
$this->install_dir = Config::get('install_dir', realpath(__DIR__ . '/../..'));
$this->install_dir = realpath(__DIR__ . '/../..');
}
public static function make(int $cache = 0): Git
@ -196,7 +196,7 @@ class Git
return $this->cacheGet('remoteCommit', function () {
if ($this->isAvailable()) {
try {
return (array) Http::client()->get(Config::get('github_api') . 'commits/master')->json();
return (array) Http::client()->get(LibrenmsConfig::get('github_api') . 'commits/master')->json();
} catch (ConnectionException $e) {
}
}

View File

@ -25,9 +25,9 @@
namespace LibreNMS\Util;
use App\ConfigRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use LibreNMS\Config;
use LibreNMS\DB\Eloquent;
use Symfony\Component\Process\Process;
@ -38,20 +38,22 @@ class Version
/** @var Git convenience instance */
public $git;
private ConfigRepository $config;
public function __construct()
public function __construct(ConfigRepository $config)
{
$this->config = $config;
$this->git = Git::make();
}
public static function get(): Version
{
return new static;
return new static(app('librenms-config'));
}
public function release(): string
{
return Config::get('update_channel') == 'master' ? 'master' : self::VERSION;
return $this->config->get('update_channel') == 'master' ? 'master' : self::VERSION;
}
public function date(string $format = 'c'): string
@ -135,7 +137,7 @@ class Version
public function rrdtool(): string
{
$process = new Process([Config::get('rrdtool', 'rrdtool'), '--version']);
$process = new Process([$this->config->get('rrdtool', 'rrdtool'), '--version']);
$process->run();
preg_match('/^RRDtool ([\w.]+) /', $process->getOutput(), $matches);
@ -144,7 +146,7 @@ class Version
public function netSnmp(): string
{
$process = new Process([Config::get('snmpget', 'snmpget'), '-V']);
$process = new Process([$this->config->get('snmpget', 'snmpget'), '-V']);
$process->run();
preg_match('/[\w.]+$/', $process->getErrorOutput(), $matches);

576
app/ConfigRepository.php Normal file
View File

@ -0,0 +1,576 @@
<?php
/**
* Config.php
*
* Config convenience class to access and set config variables.
*
* 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 2017 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App;
use App\Models\Callback;
use App\Models\GraphType;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use LibreNMS\DB\Eloquent;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Version;
use Log;
class ConfigRepository
{
private array $config;
/**
* Load the config, if the database connected, pull in database settings.
*
* return &array
*/
public function __construct()
{
// load config settings that can be cached
$cache_ttl = config('librenms.config_cache_ttl');
$this->config = Cache::driver($cache_ttl == 0 ? 'null' : 'file')->remember('librenms-config', $cache_ttl, function () {
$this->config = [];
// merge all config sources together config_definitions.json > db config > config.php
$this->loadPreUserConfigDefaults();
$this->loadDB();
$this->loadUserConfigFile($this->config);
$this->loadPostUserConfigDefaults();
return $this->config;
});
// set config settings that must change every run
$this->loadRuntimeSettings();
}
/**
* Get the config setting definitions
*
* @return array
*/
public function getDefinitions(): array
{
return json_decode(file_get_contents($this->get('install_dir') . '/misc/config_definitions.json'), true)['config'];
}
/**
* Load the user config from config.php
*
* @param array $config (this should be $this->config)
*/
private function loadUserConfigFile(&$config): void
{
// Load user config file
$file = $this->get('install_dir') . '/config.php';
if (is_file($file)) {
@include $file;
}
}
/**
* Get a config value, if non existent null (or default if set) will be returned
*
* @param string $key period separated config variable name
* @param mixed $default optional value to return if the setting is not set
* @return mixed
*/
public function get($key, $default = null): mixed
{
if (isset($this->config[$key])) {
return $this->config[$key];
}
if (! Str::contains($key, '.')) {
return $default;
}
return Arr::get($this->config, $key, $default);
}
/**
* Unset a config setting
* or multiple
*
* @param string|array $key
*/
public function forget($key): void
{
Arr::forget($this->config, $key);
}
/**
* Get a setting from a device, if that is not set,
* fall back to the global config setting prefixed by $global_prefix
* The key must be the same for the global setting and the device setting.
*
* @param array $device Device array
* @param string $key Name of setting to fetch
* @param string $global_prefix specify where the global setting lives in the global config
* @param mixed $default will be returned if the setting is not set on the device or globally
* @return mixed
*/
public function getDeviceSetting($device, $key, $global_prefix = null, $default = null): mixed
{
if (isset($device[$key])) {
return $device[$key];
}
if (isset($global_prefix)) {
$key = "$global_prefix.$key";
}
return $this->get($key, $default);
}
/**
* Get a setting from the $config['os'] array using the os of the given device
*
* @param string $os The os name
* @param string $key period separated config variable name
* @param mixed $default optional value to return if the setting is not set
* @return mixed
*/
public function getOsSetting($os, $key, $default = null): mixed
{
if ($os) {
\LibreNMS\Util\OS::loadDefinition($os);
if (isset($this->config['os'][$os][$key])) {
return $this->config['os'][$os][$key];
}
$os_key = "os.$os.$key";
if ($this->has($os_key)) {
return $this->get($os_key);
}
}
return $default;
}
/**
* Get the merged array from the global and os settings for the specified key.
* Removes any duplicates.
* When the arrays have keys, os settings take precedence over global settings
*
* @param string|null $os The os name
* @param string $key period separated config variable name
* @param string $global_prefix prefix for global setting
* @param array $default optional array to return if the setting is not set
* @return array
*/
public function getCombined(?string $os, string $key, string $global_prefix = '', array $default = []): array
{
$global_key = $global_prefix . $key;
if (! isset($this->config['os'][$os][$key])) {
if (! Str::contains($global_key, '.')) {
return (array) $this->get($global_key, $default);
}
if (! $this->has("os.$os.$key")) {
return (array) $this->get($global_key, $default);
}
}
if (! $this->has("os.$os.$key")) {
return (array) $this->get($global_key, $default);
}
return array_unique(array_merge(
(array) $this->get($global_key),
(array) $this->getOsSetting($os, $key)
));
}
/**
* Set a variable in the global config
*
* @param mixed $key period separated config variable name
* @param mixed $value
*/
public function set($key, $value): void
{
Arr::set($this->config, $key, $value);
}
/**
* Save setting to persistent storage.
*
* @param mixed $key period separated config variable name
* @param mixed $value
* @return bool if the save was successful
*/
public function persist($key, $value): bool
{
try {
Arr::set($this->config, $key, $value);
if (! Eloquent::isConnected()) {
return false; // can't save it if there is no DB
}
\App\Models\Config::updateOrCreate(['config_name' => $key], [
'config_name' => $key,
'config_value' => $value,
]);
// delete any children (there should not be any unless it is legacy)
\App\Models\Config::query()->where('config_name', 'like', "$key.%")->delete();
return true;
} catch (Exception $e) {
if (class_exists(Log::class)) {
Log::error($e);
}
if (Debug::isEnabled()) {
echo $e;
}
if ($e instanceof \Illuminate\Database\QueryException && $e->getCode() !== '42S02') {
// re-throw, else Config service provider get stuck in a loop
// if there is an error (database not connected)
// unless it is table not found (migrations have not been run yet)
throw $e;
}
return false;
}
}
/**
* Forget a key and all it's descendants from persistent storage.
* This will effectively set it back to default.
*
* @param string $key
* @return int|false
*/
public function erase($key): bool|int
{
$this->forget($key);
try {
return \App\Models\Config::withChildren($key)->delete();
} catch (Exception $e) {
return false;
}
}
/**
* Check if a setting is set
*
* @param string $key period separated config variable name
* @return bool
*/
public function has($key): bool
{
if (isset($this->config[$key])) {
return true;
}
if (! Str::contains($key, '.')) {
return false;
}
return Arr::has($this->config, $key);
}
/**
* Serialise the whole configuration to json for use in external processes.
*
* @return string
*/
public function toJson(): string
{
return json_encode($this->config);
}
/**
* Get the full configuration array
*
* @return array
*/
public function getAll(): array
{
return $this->config;
}
/**
* merge the database config with the global config,
* global config overrides db
*/
private function loadDB(): void
{
if (! Eloquent::isConnected()) {
return; // don't even try if no DB
}
try {
\App\Models\Config::get(['config_name', 'config_value'])
->each(function ($item) {
Arr::set($this->config, $item->config_name, $item->config_value);
});
} catch (QueryException $e) {
// possibly table config doesn't exist yet
}
// load graph types from the database
$this->loadGraphsFromDb($this->config);
}
private function loadGraphsFromDb(&$config): void
{
try {
$graph_types = GraphType::all()->toArray();
} catch (QueryException $e) {
// possibly table config doesn't exist yet
$graph_types = [];
}
// load graph types from the database
foreach ($graph_types as $graph) {
$g = [];
foreach ($graph as $k => $v) {
if (strpos($k, 'graph_') == 0) {
// remove leading 'graph_' from column name
$key = str_replace('graph_', '', $k);
} else {
$key = $k;
}
$g[$key] = $v;
}
$config['graph_types'][$g['type']][$g['subtype']] = $g;
}
}
/**
* Handle defaults that are set programmatically
*/
private function loadPreUserConfigDefaults(): void
{
$this->config['install_dir'] = realpath(__DIR__ . '/..');
$definitions = $this->getDefinitions();
foreach ($definitions as $path => $def) {
if (array_key_exists('default', $def)) {
Arr::set($this->config, $path, $def['default']);
}
}
// load macros from json
$macros = json_decode(file_get_contents($this->get('install_dir') . '/misc/macros.json'), true);
Arr::set($this->config, 'alert.macros.rule', $macros);
Arr::set($this->config, 'log_dir', $this->get('install_dir') . '/logs');
Arr::set($this->config, 'distributed_poller_name', php_uname('n'));
// set base_url from access URL
if (isset($_SERVER['SERVER_NAME']) && isset($_SERVER['SERVER_PORT'])) {
$port = $_SERVER['SERVER_PORT'] != 80 ? ':' . $_SERVER['SERVER_PORT'] : '';
// handle literal IPv6
$server = Str::contains($_SERVER['SERVER_NAME'], ':') ? "[{$_SERVER['SERVER_NAME']}]" : $_SERVER['SERVER_NAME'];
Arr::set($this->config, 'base_url', "http://$server$port/");
}
// graph color copying
Arr::set($this->config, 'graph_colours.mega', array_merge(
(array) Arr::get($this->config, 'graph_colours.psychedelic', []),
(array) Arr::get($this->config, 'graph_colours.manycolours', []),
(array) Arr::get($this->config, 'graph_colours.default', []),
(array) Arr::get($this->config, 'graph_colours.mixed', [])
));
}
private function loadPostUserConfigDefaults(): void
{
if (! $this->get('email_from')) {
$this->set('email_from', '"' . $this->get('project_name') . '" <' . $this->get('email_user') . '@' . php_uname('n') . '>');
}
// Define some variables if they aren't set by user definition in config_definitions.json
$this->setDefault('html_dir', '%s/html', ['install_dir']);
$this->setDefault('rrd_dir', '%s/rrd', ['install_dir']);
$this->setDefault('mib_dir', '%s/mibs', ['install_dir']);
$this->setDefault('log_dir', '%s/logs', ['install_dir']);
$this->setDefault('log_file', '%s/%s.log', ['log_dir', 'project_id']);
$this->setDefault('plugin_dir', '%s/plugins', ['html_dir']);
$this->setDefault('temp_dir', sys_get_temp_dir() ?: '/tmp');
$this->setDefault('irc_nick', '%s', ['project_name']);
$this->setDefault('irc_chan.0', '##%s', ['project_id']);
$this->setDefault('page_title_suffix', '%s', ['project_name']);
// $this->setDefault('email_from', '"%s" <%s@' . php_uname('n') . '>', ['project_name', 'email_user']); // FIXME email_from set because alerting config
// deprecated variables
$this->deprecatedVariable('rrdgraph_real_95th', 'rrdgraph_real_percentile');
$this->deprecatedVariable('fping_options.millisec', 'fping_options.interval');
$this->deprecatedVariable('discovery_modules.cisco-vrf', 'discovery_modules.vrf');
$this->deprecatedVariable('discovery_modules.toner', 'discovery_modules.printer-supplies');
$this->deprecatedVariable('poller_modules.toner', 'poller_modules.printer-supplies');
$this->deprecatedVariable('discovery_modules.cisco-sla', 'discovery_modules.slas');
$this->deprecatedVariable('poller_modules.cisco-sla', 'poller_modules.slas');
$this->deprecatedVariable('oxidized.group', 'oxidized.maps.group');
// migrate device display
if (! $this->has('device_display_default')) {
$display_value = '{{ $hostname }}';
if ($this->get('force_hostname_to_sysname')) {
$display_value = '{{ $sysName }}';
} elseif ($this->get('force_ip_to_sysname')) {
$display_value = '{{ $sysName_fallback }}';
}
$this->persist('device_display_default', $display_value);
}
// make sure we have full path to binaries in case PATH isn't set
foreach (['fping', 'fping6', 'snmpgetnext', 'rrdtool', 'traceroute'] as $bin) {
if (! is_executable($this->get($bin))) {
$this->persist($bin, $this->locateBinary($bin));
}
}
if (! $this->has('rrdtool_version')) {
$this->persist('rrdtool_version', (new Version($this))->rrdtool());
}
if (! $this->has('snmp.unescape')) {
$this->persist('snmp.unescape', version_compare((new Version($this))->netSnmp(), '5.8.0', '<'));
}
if (! self::has('reporting.usage')) {
self::persist('reporting.usage', (bool) Callback::get('enabled'));
}
// populate legacy DB credentials, just in case something external uses them. Maybe remove this later
$this->populateLegacyDbCredentials();
}
private function loadRuntimeSettings(): void
{
// If we're on SSL, let's properly detect it
if (
isset($_SERVER['HTTPS']) ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
) {
$this->set('base_url', preg_replace('/^http:/', 'https:', $this->get('base_url', '')));
}
$this->set('base_url', Str::finish($this->get('base_url', ''), '/'));
$this->set('applied_site_style', $this->get('site_style'));
$this->populateTime();
}
/**
* Set default values for defaults that depend on other settings, if they are not already loaded
*
* @param string $key
* @param string $value value to set to key or vsprintf() format string for values below
* @param array $format_values array of keys to send to vsprintf()
*/
private function setDefault($key, $value, $format_values = []): void
{
if (! $this->has($key)) {
if (is_string($value)) {
$format_values = array_map([$this, 'get'], $format_values);
$this->set($key, vsprintf($value, $format_values));
} else {
$this->set($key, $value);
}
}
}
/**
* Copy data from old variables to new ones.
*
* @param string $old
* @param string $new
*/
private function deprecatedVariable($old, $new): void
{
if ($this->has($old)) {
if (Debug::isEnabled()) {
echo "Copied deprecated config $old to $new\n";
}
$this->set($new, $this->get($old));
}
}
/**
* Locate the actual path of a binary
*
* @param string $binary
* @return mixed
*/
public function locateBinary($binary): mixed
{
if (! Str::contains($binary, '/')) {
$output = `whereis -b $binary`;
$list = trim(substr($output, strpos($output, ':') + 1));
$targets = explode(' ', $list);
foreach ($targets as $target) {
if (is_executable($target)) {
return $target;
}
}
}
return $binary;
}
private function populateTime(): void
{
$now = time();
$now -= $now % 300;
$this->set('time.now', $now);
$this->set('time.onehour', $now - 3600); // time() - (1 * 60 * 60);
$this->set('time.fourhour', $now - 14400); // time() - (4 * 60 * 60);
$this->set('time.sixhour', $now - 21600); // time() - (6 * 60 * 60);
$this->set('time.twelvehour', $now - 43200); // time() - (12 * 60 * 60);
$this->set('time.day', $now - 86400); // time() - (24 * 60 * 60);
$this->set('time.twoday', $now - 172800); // time() - (2 * 24 * 60 * 60);
$this->set('time.week', $now - 604800); // time() - (7 * 24 * 60 * 60);
$this->set('time.twoweek', $now - 1209600); // time() - (2 * 7 * 24 * 60 * 60);
$this->set('time.month', $now - 2678400); // time() - (31 * 24 * 60 * 60);
$this->set('time.twomonth', $now - 5356800); // time() - (2 * 31 * 24 * 60 * 60);
$this->set('time.threemonth', $now - 8035200); // time() - (3 * 31 * 24 * 60 * 60);
$this->set('time.sixmonth', $now - 16070400); // time() - (6 * 31 * 24 * 60 * 60);
$this->set('time.year', $now - 31536000); // time() - (365 * 24 * 60 * 60);
$this->set('time.twoyear', $now - 63072000); // time() - (2 * 365 * 24 * 60 * 60);
}
public function populateLegacyDbCredentials(): void
{
if (! class_exists('config')) {
return;
}
$db = config('database.default');
$this->set('db_host', config("database.connections.$db.host", 'localhost'));
$this->set('db_name', config("database.connections.$db.database", 'librenms'));
$this->set('db_user', config("database.connections.$db.username", 'librenms'));
$this->set('db_pass', config("database.connections.$db.password"));
$this->set('db_port', config("database.connections.$db.port", 3306));
$this->set('db_socket', config("database.connections.$db.unix_socket"));
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* LibrenmsConfig.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 2019 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Facades;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Facade;
class LibrenmsConfig extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'librenms-config';
}
public static function reload(): void
{
App::forgetInstance('librenms-config'); // clear singleton
self::clearResolvedInstances(); // clear facade resolved instances cache
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Facades\LibrenmsConfig;
use App\Models\Sensor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Relations\Relation;
@ -9,7 +10,6 @@ use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use LibreNMS\Cache\PermissionsCache;
use LibreNMS\Config;
use LibreNMS\Util\IP;
use LibreNMS\Util\Validate;
use Validator;
@ -61,13 +61,13 @@ class AppServiceProvider extends ServiceProvider
$this->bootObservers();
}
private function bootCustomBladeDirectives()
private function bootCustomBladeDirectives(): void
{
Blade::if('config', function ($key, $value = true) {
return \LibreNMS\Config::get($key) == $value;
return LibrenmsConfig::get($key) == $value;
});
Blade::if('notconfig', function ($key) {
return ! \LibreNMS\Config::get($key);
return ! LibrenmsConfig::get($key);
});
Blade::if('admin', function () {
return auth()->check() && auth()->user()->isAdmin();
@ -114,7 +114,7 @@ class AppServiceProvider extends ServiceProvider
{
$this->app->alias(\LibreNMS\Interfaces\Geocoder::class, 'geocoder');
$this->app->bind(\LibreNMS\Interfaces\Geocoder::class, function ($app) {
$engine = Config::get('geoloc.engine');
$engine = LibrenmsConfig::get('geoloc.engine');
switch ($engine) {
case 'mapquest':

View File

@ -2,8 +2,10 @@
namespace App\Providers;
use App\ConfigRepository;
use App\Facades\LibrenmsConfig;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use LibreNMS\Config;
class ConfigServiceProvider extends ServiceProvider
{
@ -14,16 +16,16 @@ class ConfigServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
}
$this->app->singleton('librenms-config', function () {
return new ConfigRepository;
});
/**
* Bootstrap services.
*
* @return void
*/
public function boot(): void
{
Config::load();
// if we skipped loading the DB the first time config was called, load it when it is available
$this->callAfterResolving('db', function () {
if ($this->app->resolved('librenms-config')) {
Log::error('Loaded config twice due to bad initialization order');
LibrenmsConfig::reload();
}
});
}
}

View File

@ -25,6 +25,7 @@
namespace App\Providers;
use App\Facades\LibrenmsConfig;
use App\Logging\Reporting\Middleware\AddGitInformation;
use App\Logging\Reporting\Middleware\CleanContext;
use App\Logging\Reporting\Middleware\SetGroups;
@ -34,7 +35,6 @@ use ErrorException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Util\Git;
use Spatie\FlareClient\Report;
use Spatie\LaravelIgnition\Facades\Flare;
@ -50,12 +50,10 @@ class ErrorReportingProvider extends \Spatie\LaravelIgnition\IgnitionServiceProv
/** @var string|null */
private static $instanceId;
private $throttle = 300;
private ?int $throttle = null;
public function boot(): void
{
$this->throttle = Config::get('reporting.throttle', 300);
/* @phpstan-ignore-next-line */
if (! method_exists(\Spatie\FlareClient\Flare::class, 'filterReportsUsing')) {
Log::debug("Flare client too old, disabling Ignition to avoid bug.\n");
@ -64,7 +62,7 @@ class ErrorReportingProvider extends \Spatie\LaravelIgnition\IgnitionServiceProv
}
Flare::filterExceptionsUsing(function (\Exception $e) {
if (Config::get('reporting.dump_errors')) {
if (LibrenmsConfig::get('reporting.dump_errors')) {
\Log::critical('%RException: ' . get_class($e) . ' ' . $e->getMessage() . '%n @ %G' . $e->getFile() . ':' . $e->getLine() . '%n' . PHP_EOL . $e->getTraceAsString(), ['color' => true]);
}
@ -108,14 +106,14 @@ class ErrorReportingProvider extends \Spatie\LaravelIgnition\IgnitionServiceProv
}
// safety check so we don't leak early reports (but reporting should not be loaded before the config is)
if (! Config::isLoaded()) {
if (! app()->bound('librenms-config')) {
return false;
}
$this->reportingEnabled = false; // don't cache before config is loaded
// check the user setting
if (Config::get('reporting.error') !== true) {
if (LibrenmsConfig::get('reporting.error') !== true) {
\Log::debug('Reporting disabled by user setting');
return false;
@ -157,6 +155,8 @@ class ErrorReportingProvider extends \Spatie\LaravelIgnition\IgnitionServiceProv
private function isThrottled(): bool
{
$this->throttle ??= LibrenmsConfig::get('reporting.throttle', 300);
if ($this->throttle) {
$this->reportingEnabled = false; // disable future reporting (to avoid this cache check)

View File

@ -211,6 +211,7 @@ return [
'PluginManager' => \App\Facades\PluginManager::class,
'Rrd' => \App\Facades\Rrd::class,
'SnmpQuery' => \App\Facades\FacadeAccessorSnmp::class,
'LibrenmsConfig' => \App\Facades\LibrenmsConfig::class,
])->forget([
'Http', // don't use Laravel Http facade, LibreNMS has its own wrapper
])->toArray(),

View File

@ -53,4 +53,13 @@
'node_id' => env('NODE_ID'),
/*
|--------------------------------------------------------------------------
| Config Cache TTL
|--------------------------------------------------------------------------
|
| Amount of seconds to allow the config to be cached. 0 means no cache.
*/
'config_cache_ttl' => env('CONFIG_CACHE_TTL', 0),
];

View File

@ -80,7 +80,7 @@ return [
'single' => [
'driver' => 'single',
'path' => env('APP_LOG', \LibreNMS\Config::get('log_file', base_path('logs/librenms.log'))),
'path' => env('APP_LOG', base_path('logs/librenms.log')),
'formatter' => \App\Logging\NoColorFormatter::class,
'level' => env('LOG_LEVEL', 'error'),
'replace_placeholders' => true,
@ -88,7 +88,7 @@ return [
'daily' => [
'driver' => 'daily',
'path' => env('APP_LOG', \LibreNMS\Config::get('log_file', base_path('logs/librenms.log'))),
'path' => env('APP_LOG', base_path('logs/librenms.log')),
'formatter' => \App\Logging\NoColorFormatter::class,
'level' => env('LOG_LEVEL', 'error'),
'days' => 14,

View File

@ -40,6 +40,7 @@
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="testing"/>
<env name="CONFIG_CACHE_TTL" value="300"/>
<const name="PHPUNIT_RUNNING" value="true"/>
</php>
</phpunit>

View File

@ -2,7 +2,7 @@
/**
* ConfigTest.php
*
* Tests for LibreNMS\Config
* Tests for App\Facades\Config
*
* 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
@ -25,17 +25,17 @@
namespace LibreNMS\Tests;
use App\ConfigRepository;
use LibreNMS\Config;
class ConfigTest extends TestCase
{
private $config;
private \ReflectionProperty $config;
protected function setUp(): void
{
parent::setUp();
$this->config = new \ReflectionProperty(Config::class, 'config');
$this->config->setAccessible(true);
$this->config = new \ReflectionProperty(ConfigRepository::class, 'config');
}
public function testGetBasic(): void
@ -46,8 +46,9 @@ class ConfigTest extends TestCase
public function testSetBasic(): void
{
$instance = $this->app->make('librenms-config');
Config::set('basics', 'first');
$this->assertEquals('first', $this->config->getValue()['basics']);
$this->assertEquals('first', $this->config->getValue($instance)['basics']);
}
public function testGet(): void
@ -137,9 +138,10 @@ class ConfigTest extends TestCase
public function testSet(): void
{
$instance = $this->app->make('librenms-config');
Config::set('you.and.me', "I'll be there");
$this->assertEquals("I'll be there", $this->config->getValue()['you']['and']['me']);
$this->assertEquals("I'll be there", $this->config->getValue($instance)['you']['and']['me']);
}
public function testSetPersist(): void
@ -206,9 +208,10 @@ class ConfigTest extends TestCase
*/
private function setConfig($function)
{
$config = $this->config->getValue();
$instance = $this->app->make('librenms-config');
$config = $this->config->getValue($instance);
$function($config);
$this->config->setValue($config);
$this->config->setValue($instance, $config);
}
public function testForget(): void

View File

@ -42,7 +42,7 @@ class OSDiscoveryTest extends TestCase
{
parent::setUpBeforeClass();
$glob = Config::get('install_dir') . '/tests/snmpsim/*.snmprec';
$glob = realpath(__DIR__ . '/..') . '/tests/snmpsim/*.snmprec';
self::$unchecked_files = array_flip(array_filter(array_map(function ($file) {
return basename($file, '.snmprec');

View File

@ -23,7 +23,6 @@
* @author Tony Murray <murraytony@gmail.com>
*/
use LibreNMS\Config;
use LibreNMS\Util\Snmpsim;
$install_dir = realpath(__DIR__ . '/..');
@ -84,7 +83,6 @@ if (getenv('DBTEST')) {
unset($db_config);
}
Config::reload(); // reload the config including database config
\LibreNMS\Util\OS::updateCache(true); // Force update of OS Cache
app()->terminate(); // destroy the bootstrap Laravel application