From ebadcbc8af8146f38d12822229b27557d78ca6e0 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Mon, 26 Apr 2021 21:03:03 -0500 Subject: [PATCH] Enable config:set to set variables inside a nested array of settings (#12772) * Enable config:set to set variables inside a nested array of settings Re-index arrays when forgetting a value from a sequential numerically indexed array * cleanup --- app/Console/Commands/SetConfigCommand.php | 104 ++++++++++++++++++++-- resources/lang/en/commands.php | 4 +- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/app/Console/Commands/SetConfigCommand.php b/app/Console/Commands/SetConfigCommand.php index 186d4c2f67..2328f59581 100644 --- a/app/Console/Commands/SetConfigCommand.php +++ b/app/Console/Commands/SetConfigCommand.php @@ -4,6 +4,8 @@ namespace App\Console\Commands; use App\Console\Commands\Traits\CompletesConfigArgument; use App\Console\LnmsCommand; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use LibreNMS\Config; use LibreNMS\DB\Eloquent; use LibreNMS\Util\DynamicConfig; @@ -39,11 +41,15 @@ class SetConfigCommand extends LnmsCommand $setting = $this->argument('setting'); $value = $this->argument('value'); $force = $this->option('ignore-checks'); + $parent = null; - if (! $force && ! $definition->isValidSetting($setting)) { - $this->error(trans('commands.config:set.errors.invalid')); + if (! $definition->isValidSetting($setting)) { + $parent = $this->findParentSetting($definition, $setting); + if (! $force && ! $parent) { + $this->error(trans('commands.config:set.errors.invalid')); - return 2; + return 2; + } } if (! Eloquent::isConnected()) { @@ -53,16 +59,41 @@ class SetConfigCommand extends LnmsCommand } if (! $force && ! $value) { - if ($this->confirm(trans('commands.config:set.confirm', ['setting' => $setting]))) { - Config::erase($setting); + $message = $parent + ? trans('commands.config:set.forget_from', ['path' => $this->getChildPath($setting, $parent), 'parent' => $parent]) + : trans('commands.config:set.confirm', ['setting' => $setting]); - return 0; + if ($this->confirm($message)) { + return $this->erase($setting, $parent) ? 0 : 1; } return 3; } $value = $this->juggleType($value); + + // handle appending to arrays + if (Str::endsWith($setting, '.+')) { + $setting = substr($setting, 0, -2); + $sub_data = Config::get($setting, []); + if (! is_array($sub_data)) { + $this->error(trans('commands.config:set.errors.append')); + + return 2; + } + + array_push($sub_data, $value); + $value = $sub_data; + } + + // handle setting value inside multi-dimensional array + if ($parent) { + $parent_data = Config::get($parent); + Arr::set($parent_data, $this->getChildPath($setting, $parent), $value); + $value = $parent_data; + $setting = $parent; + } + $configItem = $definition->get($setting); if (! $force && ! $configItem->checkValue($value)) { $message = ($configItem->type || $configItem->validate) @@ -93,4 +124,65 @@ class SetConfigCommand extends LnmsCommand return json_last_error() ? $value : $json; } + + private function findParentSetting(DynamicConfig $definition, $setting): ?string + { + $parts = explode('.', $setting); + array_pop($parts); // looking for parent, not this setting + + while (! empty($parts)) { + $name = implode('.', $parts); + if ($definition->isValidSetting($name)) { + return $name; + } + array_pop($parts); + } + + return null; + } + + private function erase($setting, $parent = null) + { + if ($parent) { + $data = Config::get($parent); + + if (preg_match("/^$parent\.?(?.+)\\.(?\\d+)\$/", $setting, $matches)) { + // nested inside the parent setting, update just the required part + $sub_data = Arr::get($data, $matches['sub']); + $this->forgetWithIndex($sub_data, $matches['index']); + Arr::set($data, $matches['sub'], $sub_data); + } else { + // not nested, just forget the setting + $this->forgetWithIndex($data, $this->getChildPath($setting, $parent)); + } + + return Config::persist($parent, $data); + } + + return Config::erase($setting); + } + + private function getChildPath($setting, $parent = null): string + { + return ltrim(Str::after($setting, $parent), '.'); + } + + private function hasSequentialIndex($array): bool + { + if (! is_array($array) || $array === []) { + return false; + } + + return array_keys($array) === range(0, count($array) - 1); + } + + private function forgetWithIndex(&$data, $matches) + { + // detect sequentially numeric indexed array so we can re-index the array + if ($this->hasSequentialIndex($data)) { + array_splice($data, (int) $matches, 1); + } else { + Arr::forget($data, $matches); + } + } } diff --git a/resources/lang/en/commands.php b/resources/lang/en/commands.php index e271907d33..8b92e8f74e 100644 --- a/resources/lang/en/commands.php +++ b/resources/lang/en/commands.php @@ -13,14 +13,16 @@ return [ 'config:set' => [ 'description' => 'Set configuration value (or unset)', 'arguments' => [ - 'setting' => 'setting to set in dot notation (example: snmp.community.0)', + 'setting' => 'setting to set in dot notation (example: snmp.community.0) To append to an array suffix with .+', 'value' => 'value to set, unset setting if this is omitted', ], 'options' => [ 'ignore-checks' => 'Ignore all safety checks', ], 'confirm' => 'Reset :setting to the default?', + 'forget_from' => 'Forget :path from :parent?', 'errors' => [ + 'append' => 'Cannot append to non-array setting', 'failed' => 'Failed to set :setting', 'invalid' => 'This is not a valid setting. Please check your spelling', 'nodb' => 'Database is not connected',