lnms config:set ability to set os settings (#13151)

* lnms config:set works for os settings
validate against os schema (gives us path and value validation)
fix unset in config:set
json formatted output in config:get to match input parsing

* inline errors

* Check that OS exists

* Fix lock file

* Set param type

* correct method name, it no longer returns a boolean

* rename --json to --dump
tests and fixes

* fix whitespace

* missed one whitespace

* typehints

* add connection typehint

* try again
This commit is contained in:
Tony Murray 2021-08-19 18:34:19 -05:00 committed by GitHub
parent fcb1f2d6fa
commit e2d1bfff54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 328 additions and 83 deletions

View File

@ -24,7 +24,7 @@ class GetConfigCommand extends LnmsCommand
parent::__construct();
$this->addArgument('setting', InputArgument::OPTIONAL);
$this->addOption('json');
$this->addOption('dump');
}
/**
@ -42,7 +42,7 @@ class GetConfigCommand extends LnmsCommand
Config::forget("os.{$matches['os']}.definition_loaded");
}
if ($this->option('json')) {
if ($this->option('dump')) {
$this->line($setting ? json_encode(Config::get($setting)) : Config::toJson());
return 0;
@ -55,7 +55,7 @@ class GetConfigCommand extends LnmsCommand
if (Config::has($setting)) {
$output = Config::get($setting);
if (! is_string($output)) {
$output = var_export($output, true);
$output = json_encode($output, JSON_PRETTY_PRINT);
}
$this->line($output);

View File

@ -6,9 +6,13 @@ use App\Console\Commands\Traits\CompletesConfigArgument;
use App\Console\LnmsCommand;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Exception\ValidationException;
use JsonSchema\Validator;
use LibreNMS\Config;
use LibreNMS\DB\Eloquent;
use LibreNMS\Util\DynamicConfig;
use LibreNMS\Util\OS;
use Symfony\Component\Console\Input\InputArgument;
class SetConfigCommand extends LnmsCommand
@ -43,7 +47,17 @@ class SetConfigCommand extends LnmsCommand
$force = $this->option('ignore-checks');
$parent = null;
if (! $definition->isValidSetting($setting)) {
if (preg_match('/^os\.(?<os>[a-z_\-]+)\.(?<setting>.*)$/', $setting, $matches)) {
$os = $matches['os'];
try {
$this->validateOsSetting($os, $matches['setting'], $value);
} catch (ValidationException $e) {
$this->error(trans('commands.config:set.errors.invalid'));
$this->line($e->getMessage());
return 2;
}
} elseif (! $definition->isValidSetting($setting)) {
$parent = $this->findParentSetting($definition, $setting);
if (! $force && ! $parent) {
$this->error(trans('commands.config:set.errors.invalid'));
@ -87,7 +101,7 @@ class SetConfigCommand extends LnmsCommand
}
// handle setting value inside multi-dimensional array
if ($parent) {
if ($parent && $parent !== $setting) {
$parent_data = Config::get($parent);
Arr::set($parent_data, $this->getChildPath($setting, $parent), $value);
$value = $parent_data;
@ -95,7 +109,10 @@ class SetConfigCommand extends LnmsCommand
}
$configItem = $definition->get($setting);
if (! $force && ! $configItem->checkValue($value)) {
if (! $force
&& empty($os) // if os is set, value was already validated against os config
&& ! $configItem->checkValue($value)
) {
$message = ($configItem->type || $configItem->validate)
? $configItem->getValidationMessage($value)
: trans('commands.config:set.errors.no-validation', ['setting' => $setting]);
@ -118,7 +135,7 @@ class SetConfigCommand extends LnmsCommand
*
* @return mixed
*/
private function juggleType(string $value)
private function juggleType(?string $value)
{
$json = json_decode($value, true);
@ -185,4 +202,54 @@ class SetConfigCommand extends LnmsCommand
Arr::forget($data, $matches);
}
}
/**
* @param string $os
* @param string $setting
* @param mixed $value
* @throws \JsonSchema\Exception\ValidationException
*/
private function validateOsSetting(string $os, string $setting, $value)
{
// prep data to be validated
OS::loadDefinition($os);
$os_data = \LibreNMS\Config::get("os.$os");
if ($os_data === null) {
throw new ValidationException(trans('commands.config:set.errors.invalid_os', ['os' => $os]));
}
Arr::set($os_data, $setting, $this->juggleType($value));
unset($os_data['definition_loaded']);
$validator = new Validator;
$validator->validate(
$os_data,
(object) ['$ref' => 'file://' . base_path('/misc/os_schema.json')],
Constraint::CHECK_MODE_TYPE_CAST
);
$code = 0;
$errors = collect($validator->getErrors())->filter(function ($error) use ($value, &$code) {
if ($error['constraint'] == 'additionalProp') {
$code = 1;
return true;
}
// only check type if value is set (otherwise we are unsetting it)
if (! empty($value) && $error['constraint'] == 'type') {
if ($code === 0) {
$code = 2; // wrong path takes precedence over wrong type
}
return true;
}
return false;
});
if ($errors->isNotEmpty()) {
throw new ValidationException($errors->pluck('message')->implode(PHP_EOL), $code);
}
}
}

View File

@ -35,6 +35,7 @@
"genealabs/laravel-caffeine": "^8.0",
"guzzlehttp/guzzle": "^7.0.1",
"influxdb/influxdb-php": "^1.14",
"justinrainbow/json-schema": "^5.2",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.0",
@ -59,7 +60,6 @@
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"friendsofphp/php-cs-fixer": "^2.16",
"justinrainbow/json-schema": "^5.2",
"laravel/dusk": "^6.15",
"mockery/mockery": "^1.4.2",
"nunomaduro/collision": "^5.0",

144
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7e036d2ebed332bf23de83ae6dd716a3",
"content-hash": "7971c0ef0ed8ea90a3df66dbd8f05295",
"packages": [
{
"name": "amenadiel/jpgraph",
@ -1729,6 +1729,76 @@
},
"time": "2020-09-11T11:05:47+00:00"
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.11",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa",
"reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
"bin": [
"bin/validate-json"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"psr-4": {
"JsonSchema\\": "src/JsonSchema/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bruno Prieto Reis",
"email": "bruno.p.reis@gmail.com"
},
{
"name": "Justin Rainbow",
"email": "justin.rainbow@gmail.com"
},
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
},
{
"name": "Robert Schönthal",
"email": "seroscho@googlemail.com"
}
],
"description": "A library to validate a json schema.",
"homepage": "https://github.com/justinrainbow/json-schema",
"keywords": [
"json",
"schema"
],
"support": {
"issues": "https://github.com/justinrainbow/json-schema/issues",
"source": "https://github.com/justinrainbow/json-schema/tree/5.2.11"
},
"time": "2021-07-22T09:24:00+00:00"
},
{
"name": "laravel/framework",
"version": "v8.49.2",
@ -8260,76 +8330,6 @@
},
"time": "2020-07-09T08:09:16+00:00"
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.10",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
"reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
"bin": [
"bin/validate-json"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"psr-4": {
"JsonSchema\\": "src/JsonSchema/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bruno Prieto Reis",
"email": "bruno.p.reis@gmail.com"
},
{
"name": "Justin Rainbow",
"email": "justin.rainbow@gmail.com"
},
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
},
{
"name": "Robert Schönthal",
"email": "seroscho@googlemail.com"
}
],
"description": "A library to validate a json schema.",
"homepage": "https://github.com/justinrainbow/json-schema",
"keywords": [
"json",
"schema"
],
"support": {
"issues": "https://github.com/justinrainbow/json-schema/issues",
"source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
},
"time": "2020-05-27T16:41:55+00:00"
},
{
"name": "laravel/dusk",
"version": "v6.15.1",
@ -11460,5 +11460,5 @@
"ext-xml": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -70,7 +70,7 @@ lnms config:set snmp.community
> yes
lnms config:get snmp.community --json
lnms config:get snmp.community
["public"]
```

View File

@ -7,7 +7,7 @@ return [
'setting' => 'setting to get value of in dot notation (example: snmp.community.0)',
],
'options' => [
'json' => 'Output setting or entire config as json',
'dump' => 'Output the entire config as json',
],
],
'config:set' => [
@ -24,7 +24,8 @@ return [
'errors' => [
'append' => 'Cannot append to non-array setting',
'failed' => 'Failed to set :setting',
'invalid' => 'This is not a valid setting. Please check your spelling',
'invalid' => 'This is not a valid setting. Please check your input',
'invalid_os' => 'Specified OS (:os) does not exist',
'nodb' => 'Database is not connected',
'no-validation' => 'Cannot set :setting, it is missing validation definition.',
],

View File

@ -0,0 +1,135 @@
<?php
/*
* TestSetConfigCommand.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\Tests\Feature\Commands;
use LibreNMS\Config;
use LibreNMS\Tests\InMemoryDbTestCase;
class TestConfigCommands extends InMemoryDbTestCase
{
public function testSetting(): void
{
// simple
Config::set('login_message', null);
$this->assertCliSets('login_message', 'hello');
// nested
Config::forget('allow_entity_sensor.amperes');
$this->assertCliSets('allow_entity_sensor.amperes', 'false');
// set inside
$this->assertCliGets('auth_ldap_groups.somegroup', null);
$this->artisan('config:set', ['setting' => 'auth_ldap_groups.somegroup', 'value' => '{"level": 3}'])->assertExitCode(0);
$this->assertCliGets('auth_ldap_groups.somegroup', ['level' => 3]);
$this->artisan('config:set', ['setting' => 'auth_ldap_groups.somegroup'])
->expectsConfirmation(trans('commands.config:set.forget_from', ['path' => 'somegroup', 'parent' => 'auth_ldap_groups']), 'yes')
->assertExitCode(0);
// test append
$community = Config::get('snmp.community');
$this->assertCliGets('snmp.community', $community);
$community[] = 'extra_community';
$this->artisan('config:set', ['setting' => 'snmp.community.+', 'value' => 'extra_community'])->assertExitCode(0);
$this->assertCliGets('snmp.community', $community);
// os bool
$this->assertCliSets('os.ios.rfc1628_compat', true);
// os array and append
$this->assertCliSets('os.netonix.bad_iftype', ['ethernet', 'psuedowire']);
// $this->artisan('config:set', ['setting' => 'os.netonix.bad_iftype.+', 'value' => 'other'])->assertExitCode(0);
// $this->assertCliGets('os.netonix.bad_iftype', ['ethernet', 'psuedowire', 'other']);
// dump
$this->artisan('config:get', ['--dump' => true])
->expectsOutput(Config::toJson())
->assertExitCode(0);
}
public function testInvalidSetting(): void
{
// non-existent setting
$this->artisan('config:set', ['setting' => 'this_will_never_be.a.setting'])
->assertExitCode(2);
// invalid type
$this->artisan('config:set', ['setting' => 'alert_rule.interval', 'value' => 'string', '--no-ansi' => true])
->expectsOutput(trans('settings.validate.integer', ['value' => '"string"']))
->assertExitCode(2);
// non-existent os
$this->artisan('config:set', ['setting' => 'os.someos.this_will_never_be.a.setting'])
->expectsOutput(trans('commands.config:set.errors.invalid_os', ['os' => 'someos']))
->assertExitCode(2);
// non-existent os setting
$this->artisan('config:set', ['setting' => 'os.ios.this_will_never_be.a.setting'])
->doesntExpectOutput(trans('commands.config:set.errors.invalid_os', ['os' => 'ios']))
->assertExitCode(2);
// append to non-array
Config::set('login_message', 'blah');
$message = Config::get('login_message');
$this->artisan('config:set', ['setting' => 'login_message.+', 'value' => 'something', '--no-ansi' => true])
->expectsOutput(trans('commands.config:set.errors.append'))
->assertExitCode(2);
}
/**
* @param string $setting
* @param mixed $expected
*/
private function assertCliSets(string $setting, $expected): void
{
$this->assertCliGets($setting, null);
$this->artisan('config:set', ['setting' => $setting, 'value' => json_encode($expected)])->assertExitCode(0);
$this->assertCliGets($setting, $expected);
$this->artisan('config:set', ['setting' => $setting])
->expectsQuestion(trans('commands.config:set.confirm', ['setting' => $setting]), true)
->assertExitCode(0);
$this->assertCliGets($setting, null);
}
/**
* @param string $setting
* @param mixed $expected
*/
private function assertCliGets(string $setting, $expected): void
{
$this->assertSame($expected, \LibreNMS\Config::get($setting));
$command = $this->artisan('config:get', ['setting' => $setting]);
if ($expected === null) {
$command->assertExitCode(1);
return;
}
$command->assertExitCode(0)
->expectsOutput(is_string($expected) ? $expected : json_encode($expected, JSON_PRETTY_PRINT))
->assertExitCode(0);
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* InMemoryDbTestCase.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\Tests;
class InMemoryDbTestCase extends TestCase
{
/** @var string */
protected $connection = 'testing_memory';
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh', ['--database' => $this->connection]);
$current = config('database.default');
config(['database.default' => $this->connection]);
\DB::purge($current);
}
}