From 7a0d604cdd6a11d481d2615722d638e3c8f014b1 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Fri, 10 Jun 2022 16:25:33 -0500 Subject: [PATCH] Automatic fixes for validation failures (#13930) * Automatic fixes for validations * webui * lint fixes * Fix an install issue with ConfigSeeder requesting cli input in web page. * Do not use c_echo in validate.php print_fail() --- LibreNMS/Interfaces/Validation.php | 43 ++ LibreNMS/Interfaces/ValidationFixer.php | 36 ++ LibreNMS/Interfaces/ValidationGroup.php | 16 +- LibreNMS/ValidationResult.php | 29 ++ LibreNMS/Validations/BaseValidation.php | 34 +- LibreNMS/Validations/Configuration.php | 2 +- LibreNMS/Validations/Database.php | 442 +----------------- .../Database/CheckDatabaseSchemaVersion.php | 95 ++++ .../Database/CheckDatabaseServerVersion.php | 70 +++ .../Database/CheckDatabaseTableNamesCase.php | 59 +++ .../Validations/Database/CheckMysqlEngine.php | 95 ++++ .../Database/CheckSchemaCollation.php | 100 ++++ .../Database/CheckSchemaStructure.php | 329 +++++++++++++ .../Database/CheckSqlServerTime.php | 68 +++ LibreNMS/Validations/Dependencies.php | 2 +- LibreNMS/Validations/Disk.php | 2 +- LibreNMS/Validations/DistributedPoller.php | 11 +- LibreNMS/Validations/Mail.php | 2 +- LibreNMS/Validations/Php.php | 2 +- LibreNMS/Validations/Programs.php | 2 +- LibreNMS/Validations/Python.php | 2 +- LibreNMS/Validations/Rrd.php | 61 +-- .../Rrd/CheckRrdDirPermissions.php | 62 +++ LibreNMS/Validations/Rrd/CheckRrdVersion.php | 73 +++ .../Rrd/CheckRrdcachedConnectivity.php | 64 +++ LibreNMS/Validations/RrdCheck.php | 2 +- LibreNMS/Validations/System.php | 2 +- LibreNMS/Validations/Updates.php | 2 +- LibreNMS/Validations/User.php | 2 +- LibreNMS/Validator.php | 11 + app/Http/Controllers/ValidateController.php | 21 + config/filesystems.php | 5 + database/seeders/ConfigSeeder.php | 2 +- phpstan-baseline.neon | 320 ------------- resources/lang/en/validation.php | 54 +++ resources/views/validate/index.blade.php | 34 +- routes/web.php | 1 + tests/Unit/ValidationFixTest.php | 58 +++ validate.php | 4 +- 39 files changed, 1374 insertions(+), 845 deletions(-) create mode 100644 LibreNMS/Interfaces/Validation.php create mode 100644 LibreNMS/Interfaces/ValidationFixer.php create mode 100644 LibreNMS/Validations/Database/CheckDatabaseSchemaVersion.php create mode 100644 LibreNMS/Validations/Database/CheckDatabaseServerVersion.php create mode 100644 LibreNMS/Validations/Database/CheckDatabaseTableNamesCase.php create mode 100644 LibreNMS/Validations/Database/CheckMysqlEngine.php create mode 100644 LibreNMS/Validations/Database/CheckSchemaCollation.php create mode 100644 LibreNMS/Validations/Database/CheckSchemaStructure.php create mode 100644 LibreNMS/Validations/Database/CheckSqlServerTime.php create mode 100644 LibreNMS/Validations/Rrd/CheckRrdDirPermissions.php create mode 100644 LibreNMS/Validations/Rrd/CheckRrdVersion.php create mode 100644 LibreNMS/Validations/Rrd/CheckRrdcachedConnectivity.php create mode 100644 tests/Unit/ValidationFixTest.php diff --git a/LibreNMS/Interfaces/Validation.php b/LibreNMS/Interfaces/Validation.php new file mode 100644 index 0000000000..924ac0f198 --- /dev/null +++ b/LibreNMS/Interfaces/Validation.php @@ -0,0 +1,43 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Interfaces; + +use LibreNMS\ValidationResult; + +interface Validation +{ + /** + * Validate this module. + */ + public function validate(): ValidationResult; + + /** + * If this validation is enabled or not. + * + * @return bool + */ + public function enabled(): bool; +} diff --git a/LibreNMS/Interfaces/ValidationFixer.php b/LibreNMS/Interfaces/ValidationFixer.php new file mode 100644 index 0000000000..2b3f49f4ff --- /dev/null +++ b/LibreNMS/Interfaces/ValidationFixer.php @@ -0,0 +1,36 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Interfaces; + +interface ValidationFixer +{ + /** + * Fix the failed validation result. Take care not to break user installs. + * + * @return bool + */ + public function fix(): bool; +} diff --git a/LibreNMS/Interfaces/ValidationGroup.php b/LibreNMS/Interfaces/ValidationGroup.php index daf2631855..dd3c0eaca4 100644 --- a/LibreNMS/Interfaces/ValidationGroup.php +++ b/LibreNMS/Interfaces/ValidationGroup.php @@ -32,29 +32,21 @@ interface ValidationGroup /** * Validate this module. * To return ValidationResults, call ok, warn, fail, or result methods on the $validator - * - * @param Validator $validator */ - public function validate(Validator $validator); + public function validate(Validator $validator): void; /** * Returns if this test should be run by default or not. - * - * @return bool */ - public function isDefault(); + public function isDefault(): bool; /** * Returns true if this group has been run - * - * @return bool */ - public function isCompleted(); + public function isCompleted(): bool; /** * Mark this group as completed - * - * @return void */ - public function markCompleted(); + public function markCompleted(): void; } diff --git a/LibreNMS/ValidationResult.php b/LibreNMS/ValidationResult.php index 6164bdf9b1..bae754766c 100644 --- a/LibreNMS/ValidationResult.php +++ b/LibreNMS/ValidationResult.php @@ -44,6 +44,8 @@ class ValidationResult private $list; /** @var string|null */ private $fix; + /** @var string|null */ + private $fixer; /** * ValidationResult constructor. @@ -226,6 +228,7 @@ class ValidationResult 'statusText' => substr($this->getStatusText($resultStatus), 2, -2), // remove console colors 'message' => $this->getMessage(), 'fix' => Arr::wrap($resultFix), + 'fixer' => $this->getFixer(), 'listDescription' => $this->getListDescription(), 'list' => is_array($resultList) ? array_values($resultList) : [], ]; @@ -249,4 +252,30 @@ class ValidationResult printf($format, " and $extra more..."); } } + + /** + * Fixer exists + */ + public function hasFixer(): bool + { + return $this->fixer !== null; + } + + /** + * @return string|null the class of the fixer + */ + public function getFixer(): ?string + { + return $this->fixer; + } + + /** + * Set fixer, optionally denote if this is fixable + */ + public function setFixer(string $fixer, bool $fixable = true): ValidationResult + { + $this->fixer = $fixable ? $fixer : null; + + return $this; + } } diff --git a/LibreNMS/Validations/BaseValidation.php b/LibreNMS/Validations/BaseValidation.php index 12d2f28ec8..68c85bad92 100644 --- a/LibreNMS/Validations/BaseValidation.php +++ b/LibreNMS/Validations/BaseValidation.php @@ -25,39 +25,55 @@ namespace LibreNMS\Validations; +use LibreNMS\Interfaces\Validation; use LibreNMS\Interfaces\ValidationGroup; +use LibreNMS\Validator; abstract class BaseValidation implements ValidationGroup { + /** @var bool */ protected $completed = false; + /** @var bool */ protected static $RUN_BY_DEFAULT = true; + /** @var string */ + protected $directory = null; + /** @var string */ + protected $name = null; + + public function validate(Validator $validator): void + { + if ($this->directory) { + foreach (glob(__DIR__ . "/$this->directory/*.php") as $file) { + $base = basename($file, '.php'); + $class = __NAMESPACE__ . "\\$this->directory\\$base"; + $validation = new $class; + if ($validation instanceof Validation && $validation->enabled()) { + $validator->result($validation->validate(), $this->name); + } + } + } + } /** * Returns if this test should be run by default or not. - * - * @return bool */ - public function isDefault() + public function isDefault(): bool { return static::$RUN_BY_DEFAULT; } /** * Returns true if this group has been run - * - * @return bool */ - public function isCompleted() + public function isCompleted(): bool { return $this->completed; } /** * Mark this group as completed - * - * @return void */ - public function markCompleted() + public function markCompleted(): void { $this->completed = true; } diff --git a/LibreNMS/Validations/Configuration.php b/LibreNMS/Validations/Configuration.php index eae5bccb2b..b7e2ac0269 100644 --- a/LibreNMS/Validations/Configuration.php +++ b/LibreNMS/Validations/Configuration.php @@ -38,7 +38,7 @@ class Configuration extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { // Test transports if (Config::get('alerts.email.enable') == true) { diff --git a/LibreNMS/Validations/Database.php b/LibreNMS/Validations/Database.php index d311b465cb..c2e2121649 100644 --- a/LibreNMS/Validations/Database.php +++ b/LibreNMS/Validations/Database.php @@ -25,439 +25,33 @@ namespace LibreNMS\Validations; -use Carbon\Carbon; -use Carbon\CarbonInterval; -use LibreNMS\Config; -use LibreNMS\DB\Eloquent; -use LibreNMS\DB\Schema; -use LibreNMS\ValidationResult; +use LibreNMS\Validations\Database\CheckDatabaseServerVersion; +use LibreNMS\Validations\Database\CheckDatabaseTableNamesCase; +use LibreNMS\Validations\Database\CheckMysqlEngine; +use LibreNMS\Validations\Database\CheckSqlServerTime; use LibreNMS\Validator; -use Symfony\Component\Yaml\Yaml; class Database extends BaseValidation { - const MYSQL_MIN_VERSION = '5.7.7'; - const MYSQL_MIN_VERSION_DATE = 'March, 2021'; - const MYSQL_RECOMMENDED_VERSION = '8.0'; + public const MYSQL_MIN_VERSION = '5.7.7'; + public const MYSQL_MIN_VERSION_DATE = 'March, 2021'; + public const MYSQL_RECOMMENDED_VERSION = '8.0'; - const MARIADB_MIN_VERSION = '10.2.2'; - const MARIADB_MIN_VERSION_DATE = 'March, 2021'; - const MARIADB_RECOMMENDED_VERSION = '10.5'; + public const MARIADB_MIN_VERSION = '10.2.2'; + public const MARIADB_MIN_VERSION_DATE = 'March, 2021'; + public const MARIADB_RECOMMENDED_VERSION = '10.5'; - public function validate(Validator $validator) - { - if (! Eloquent::isConnected()) { - return; - } - - $this->validateSystem($validator); - - if ($this->checkSchemaVersion($validator)) { - $this->checkSchema($validator); - $this->checkCollation($validator); - } - } - - public function validateSystem(Validator $validator) - { - $this->checkVersion($validator); - $this->checkMode($validator); - $this->checkTime($validator); - $this->checkMysqlEngine($validator); - } - - private function checkSchemaVersion(Validator $validator): bool - { - $current = \LibreNMS\DB\Schema::getLegacySchema(); - $latest = 1000; - - if ($current === 0 || $current === $latest) { - // Using Laravel migrations - if (! Schema::isCurrent()) { - $validator->fail('Your database is out of date!', './lnms migrate'); - - return false; - } - - $migrations = Schema::getUnexpectedMigrations(); - if ($migrations->isNotEmpty()) { - $validator->warn('Your database schema has extra migrations (' . $migrations->implode(', ') . - '). If you just switched to the stable release from the daily release, your database is in between releases and this will be resolved with the next release.'); - } - } elseif ($current < $latest) { - $validator->fail( - "Your database schema ($current) is older than the latest ($latest).", - 'Manually run ./daily.sh, and check for any errors.' - ); - - return false; - } elseif ($current > $latest) { - $validator->warn("Your database schema ($current) is newer than expected ($latest). If you just switched to the stable release from the daily release, your database is in between releases and this will be resolved with the next release."); - } - - return true; - } - - private function checkVersion(Validator $validator) - { - $version = \LibreNMS\Util\Version::get()->databaseServer(); - $version = explode('-', $version); - - if (isset($version[1]) && $version[1] == 'MariaDB') { - if (version_compare($version[0], self::MARIADB_MIN_VERSION, '<=')) { - $validator->fail( - 'MariaDB version ' . self::MARIADB_MIN_VERSION . ' is the minimum supported version as of ' . - self::MARIADB_MIN_VERSION_DATE . '.', - 'Update MariaDB to a supported version, ' . self::MARIADB_RECOMMENDED_VERSION . ' suggested.' - ); - } - } else { - if (version_compare($version[0], self::MYSQL_MIN_VERSION, '<=')) { - $validator->fail( - 'MySQL version ' . self::MYSQL_MIN_VERSION . ' is the minimum supported version as of ' . - self::MYSQL_MIN_VERSION_DATE . '.', - 'Update MySQL to a supported version, ' . self::MYSQL_RECOMMENDED_VERSION . ' suggested.' - ); - } - } - } - - private function checkTime(Validator $validator) - { - $raw_time = Eloquent::DB()->selectOne('SELECT NOW() as time')->time; - $db_time = new Carbon($raw_time); - $php_time = Carbon::now(); - - $diff = $db_time->diffAsCarbonInterval($php_time); - - if ($diff->compare(CarbonInterval::minute(1)) > 0) { - $message = "Time between this server and the mysql database is off\n"; - $message .= ' Mysql time ' . $db_time->toDateTimeString() . PHP_EOL; - $message .= ' PHP time ' . $php_time->toDateTimeString() . PHP_EOL; - - $validator->fail($message); - } - } - - private function checkMode(Validator $validator) - { - // Test for lower case table name support - $lc_mode = Eloquent::DB()->selectOne('SELECT @@global.lower_case_table_names as mode')->mode; - if ($lc_mode != 0) { - $validator->fail( - 'You have lower_case_table_names set to 1 or true in mysql config.', - 'Set lower_case_table_names=0 in your mysql config file in the [mysqld] section.' - ); - } - } - - private function checkMysqlEngine(Validator $validator) - { - $db = \config('database.connections.' . \config('database.default') . '.database'); - $query = "SELECT `TABLE_NAME` FROM information_schema.tables WHERE `TABLE_SCHEMA` = '$db' && `ENGINE` != 'InnoDB'"; - $tables = Eloquent::DB()->select($query); - if (! empty($tables)) { - $validator->result( - ValidationResult::warn('Some tables are not using the recommended InnoDB engine, this may cause you issues.') - ->setList('Tables', array_column($tables, 'TABLE_NAME')) - ); - } - } - - private function checkCollation(Validator $validator) - { - $db_name = Eloquent::DB()->selectOne('SELECT DATABASE() as name')->name; - - // Test for correct character set and collation - $db_collation_sql = "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME - FROM information_schema.SCHEMATA S - WHERE schema_name = '$db_name' AND - ( DEFAULT_CHARACTER_SET_NAME != 'utf8mb4' OR DEFAULT_COLLATION_NAME != 'utf8mb4_unicode_ci')"; - $collation = Eloquent::DB()->selectOne($db_collation_sql); - if (empty($collation) !== true) { - $validator->fail( - "MySQL Database collation is wrong: $collation->DEFAULT_CHARACTER_SET_NAME $collation->DEFAULT_COLLATION_NAME", - 'Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.' - ); - } - - $table_collation_sql = "SELECT T.TABLE_NAME, C.CHARACTER_SET_NAME, C.COLLATION_NAME - FROM information_schema.TABLES AS T, information_schema.COLLATION_CHARACTER_SET_APPLICABILITY AS C - WHERE C.collation_name = T.table_collation AND T.table_schema = '$db_name' AND - ( C.CHARACTER_SET_NAME != 'utf8mb4' OR C.COLLATION_NAME != 'utf8mb4_unicode_ci' );"; - $collation_tables = Eloquent::DB()->select($table_collation_sql); - if (empty($collation_tables) !== true) { - $result = ValidationResult::fail('MySQL tables collation is wrong: ') - ->setFix('Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.') - ->setList('Tables', array_map(function ($row) { - return "$row->TABLE_NAME $row->CHARACTER_SET_NAME $row->COLLATION_NAME"; - }, $collation_tables)); - $validator->result($result); - } - - $column_collation_sql = "SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME - FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$db_name' AND - ( CHARACTER_SET_NAME != 'utf8mb4' OR COLLATION_NAME != 'utf8mb4_unicode_ci' );"; - $collation_columns = Eloquent::DB()->select($column_collation_sql); - if (empty($collation_columns) !== true) { - $result = ValidationResult::fail('MySQL column collation is wrong: ') - ->setFix('Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.') - ->setList('Columns', array_map(function ($row) { - return "$row->TABLE_NAME: $row->COLUMN_NAME $row->CHARACTER_SET_NAME $row->COLLATION_NAME"; - }, $collation_columns)); - $validator->result($result); - } - } - - private function checkSchema(Validator $validator) - { - $schema_file = Config::get('install_dir') . '/misc/db_schema.yaml'; - - if (! is_file($schema_file)) { - $validator->warn("We haven't detected the db_schema.yaml file"); - - return; - } - - $master_schema = Yaml::parse(file_get_contents($schema_file)); - $current_schema = Schema::dump(); - $schema_update = []; - - foreach ((array) $master_schema as $table => $data) { - if (empty($current_schema[$table])) { - $validator->fail("Database: missing table ($table)"); - $schema_update[] = $this->addTableSql($table, $data); - } else { - $current_columns = array_reduce($current_schema[$table]['Columns'], function ($array, $item) { - $array[$item['Field']] = $item; - - return $array; - }, []); - - foreach ($data['Columns'] as $index => $cdata) { - $column = $cdata['Field']; - - // MySQL 8 fix, remove DEFAULT_GENERATED from timestamp extra columns - if ($cdata['Type'] == 'timestamp') { - $current_columns[$column]['Extra'] = preg_replace('/DEFAULT_GENERATED[ ]*/', '', $current_columns[$column]['Extra']); - } - - if (empty($current_columns[$column])) { - $validator->fail("Database: missing column ($table/$column)"); - $primary = false; - if ($data['Indexes']['PRIMARY']['Columns'] == [$column]) { - // include the primary index with the add statement - unset($data['Indexes']['PRIMARY']); - $primary = true; - } - $schema_update[] = $this->addColumnSql($table, $cdata, isset($data['Columns'][$index - 1]) ? $data['Columns'][$index - 1]['Field'] : null, $primary); - } elseif ($cdata !== $current_columns[$column]) { - $validator->fail("Database: incorrect column ($table/$column)"); - $schema_update[] = $this->updateTableSql($table, $column, $cdata); - } - - unset($current_columns[$column]); // remove checked columns - } - - foreach ($current_columns as $column => $_unused) { - $validator->fail("Database: extra column ($table/$column)"); - $schema_update[] = $this->dropColumnSql($table, $column); - } - - $index_changes = []; - if (isset($data['Indexes'])) { - foreach ($data['Indexes'] as $name => $index) { - if (empty($current_schema[$table]['Indexes'][$name])) { - $validator->fail("Database: missing index ($table/$name)"); - $index_changes[] = $this->addIndexSql($table, $index); - } elseif ($index != $current_schema[$table]['Indexes'][$name]) { - $validator->fail("Database: incorrect index ($table/$name)"); - $index_changes[] = $this->updateIndexSql($table, $name, $index); - } - - unset($current_schema[$table]['Indexes'][$name]); - } - } - - if (isset($current_schema[$table]['Indexes'])) { - foreach ($current_schema[$table]['Indexes'] as $name => $_unused) { - $validator->fail("Database: extra index ($table/$name)"); - $schema_update[] = $this->dropIndexSql($table, $name); - } - } - $schema_update = array_merge($schema_update, $index_changes); // drop before create/update - - $constraint_changes = []; - if (isset($data['Constraints'])) { - foreach ($data['Constraints'] as $name => $constraint) { - if (empty($current_schema[$table]['Constraints'][$name])) { - $validator->fail("Database: missing constraint ($table/$name)"); - $constraint_changes[] = $this->addConstraintSql($table, $constraint); - } elseif ($constraint != $current_schema[$table]['Constraints'][$name]) { - $validator->fail("Database: incorrect constraint ($table/$name)"); - $constraint_changes[] = $this->dropConstraintSql($table, $name); - $constraint_changes[] = $this->addConstraintSql($table, $constraint); - } - - unset($current_schema[$table]['Constraints'][$name]); - } - } - - if (isset($current_schema[$table]['Constraints'])) { - foreach ($current_schema[$table]['Constraints'] as $name => $_unused) { - $validator->fail("Database: extra constraint ($table/$name)"); - $schema_update[] = $this->dropConstraintSql($table, $name); - } - } - $schema_update = array_merge($schema_update, $constraint_changes); // drop before create/update - } - - unset($current_schema[$table]); // remove checked tables - } - - foreach ($current_schema as $table => $data) { - $validator->fail("Database: extra table ($table)"); - $schema_update[] = $this->dropTableSql($table); - } - - // set utc timezone if timestamp issues - if (preg_grep('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', $schema_update)) { - array_unshift($schema_update, "SET TIME_ZONE='+00:00';"); - } - - if (empty($schema_update)) { - $validator->ok('Database schema correct'); - } else { - $result = ValidationResult::fail('We have detected that your database schema may be wrong') - ->setFix('Run the following SQL statements to fix it') - ->setList('SQL Statements', $schema_update); - $validator->result($result); - } - } - - private function addTableSql($table, $table_schema) - { - $columns = array_map([$this, 'columnToSql'], $table_schema['Columns']); - $indexes = array_map([$this, 'indexToSql'], isset($table_schema['Indexes']) ? $table_schema['Indexes'] : []); - - $def = implode(', ', array_merge(array_values((array) $columns), array_values((array) $indexes))); - - return "CREATE TABLE `$table` ($def);"; - } - - private function addColumnSql($table, $schema, $previous_column, $primary = false) - { - $sql = "ALTER TABLE `$table` ADD " . $this->columnToSql($schema); - if ($primary) { - $sql .= ' PRIMARY KEY'; - } - if (empty($previous_column)) { - $sql .= ' FIRST'; - } else { - $sql .= " AFTER `$previous_column`"; - } - - return $sql . ';'; - } - - private function updateTableSql($table, $column, $column_schema) - { - return "ALTER TABLE `$table` CHANGE `$column` " . $this->columnToSql($column_schema) . ';'; - } - - private function dropColumnSql($table, $column) - { - return "ALTER TABLE `$table` DROP `$column`;"; - } - - private function addIndexSql($table, $index_schema) - { - return "ALTER TABLE `$table` ADD " . $this->indexToSql($index_schema) . ';'; - } - - private function updateIndexSql($table, $name, $index_schema) - { - return "ALTER TABLE `$table` DROP INDEX `$name`, " . $this->indexToSql($index_schema) . ';'; - } - - private function dropIndexSql($table, $name) - { - return "ALTER TABLE `$table` DROP INDEX `$name`;"; - } - - private function dropTableSql($table) - { - return "DROP TABLE `$table`;"; - } + protected $directory = 'Database'; + protected $name = 'database'; /** - * Generate an SQL segment to create the column based on data from Schema::dump() - * - * @param array $column_data The array of data for the column - * @return string sql fragment, for example: "`ix_id` int(10) unsigned NOT NULL" + * Tests used by the installer to validate that SQL server doesn't have any known issues (before migrations) */ - private function columnToSql($column_data) + public function validateSystem(Validator $validator): void { - $segments = ["`${column_data['Field']}`", $column_data['Type']]; - - $segments[] = $column_data['Null'] ? 'NULL' : 'NOT NULL'; - - if (isset($column_data['Default'])) { - if ($column_data['Default'] === 'CURRENT_TIMESTAMP') { - $segments[] = 'DEFAULT CURRENT_TIMESTAMP'; - } elseif ($column_data['Default'] == 'NULL') { - $segments[] = 'DEFAULT NULL'; - } else { - $segments[] = "DEFAULT '${column_data['Default']}'"; - } - } - - if ($column_data['Extra'] == 'on update current_timestamp()') { - $segments[] = 'on update CURRENT_TIMESTAMP'; - } else { - $segments[] = $column_data['Extra']; - } - - return implode(' ', $segments); - } - - /** - * Generate an SQL segment to create the index based on data from Schema::dump() - * - * @param array $index_data The array of data for the index - * @return string sql fragment, for example: "PRIMARY KEY (`device_id`)" - */ - private function indexToSql($index_data) - { - if ($index_data['Name'] == 'PRIMARY') { - $index = 'PRIMARY KEY (%s)'; - } elseif ($index_data['Unique']) { - $index = "UNIQUE `{$index_data['Name']}` (%s)"; - } else { - $index = "INDEX `{$index_data['Name']}` (%s)"; - } - - $columns = implode(',', array_map(function ($col) { - return "`$col`"; - }, $index_data['Columns'])); - - return sprintf($index, $columns); - } - - private function addConstraintSql($table, $constraint) - { - $sql = "ALTER TABLE `$table` ADD CONSTRAINT `{$constraint['name']}` FOREIGN KEY (`{$constraint['foreign_key']}`) "; - $sql .= " REFERENCES `{$constraint['table']}` (`{$constraint['key']}`)"; - if (! empty($constraint['extra'])) { - $sql .= ' ' . $constraint['extra']; - } - $sql .= ';'; - - return $sql; - } - - private function dropConstraintSql($table, $name) - { - return "ALTER TABLE `$table` DROP FOREIGN KEY `$name`;"; + $validator->result((new CheckDatabaseServerVersion)->validate(), $this->name); + $validator->result((new CheckMysqlEngine)->validate(), $this->name); + $validator->result((new CheckSqlServerTime)->validate(), $this->name); + $validator->result((new CheckDatabaseTableNamesCase)->validate(), $this->name); } } diff --git a/LibreNMS/Validations/Database/CheckDatabaseSchemaVersion.php b/LibreNMS/Validations/Database/CheckDatabaseSchemaVersion.php new file mode 100644 index 0000000000..213010fd20 --- /dev/null +++ b/LibreNMS/Validations/Database/CheckDatabaseSchemaVersion.php @@ -0,0 +1,95 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use LibreNMS\DB\Eloquent; +use LibreNMS\DB\Schema; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Interfaces\ValidationFixer; +use LibreNMS\ValidationResult; + +class CheckDatabaseSchemaVersion implements Validation, ValidationFixer +{ + /** @var bool|null */ + private static $current = null; + + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + self::$current = false; + $current = \LibreNMS\DB\Schema::getLegacySchema(); + $latest = 1000; + + if ($current === 0 || $current === $latest) { + // Using Laravel migrations + if (! Schema::isCurrent()) { + return ValidationResult::fail(trans('validation.validations.database.CheckSchemaVersion.fail_outdated'), './lnms migrate') + ->setFixer(__CLASS__); + } + + $migrations = Schema::getUnexpectedMigrations(); + if ($migrations->isNotEmpty()) { + return ValidationResult::warn(trans('validation.validations.database.CheckSchemaVersion.warn_extra_migrations', ['migrations' => $migrations->implode(', ')])); + } + } elseif ($current < $latest) { + return ValidationResult::fail( + trans('validation.validations.database.CheckSchemaVersion.fail_legacy_outdated', ['current' => $current, 'latest' => $latest]), + trans('validation.validations.database.CheckSchemaVersion.fix_legacy_outdated'), + ); + } else { + // latest > current + return ValidationResult::warn(trans('validation.validations.database.CheckSchemaVersion.warn_legacy_newer')); + } + + self::$current = true; + + return ValidationResult::ok(trans('validation.validations.database.CheckSchemaVersion.ok')); + } + + public static function isCurrent(): bool + { + if (self::$current === null) { + (new static)->validate(); + } + + return self::$current; + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected(); + } + + public function fix(): bool + { + return \Artisan::call('migrate', ['--force' => true]) === 0; + } +} diff --git a/LibreNMS/Validations/Database/CheckDatabaseServerVersion.php b/LibreNMS/Validations/Database/CheckDatabaseServerVersion.php new file mode 100644 index 0000000000..72d05475ee --- /dev/null +++ b/LibreNMS/Validations/Database/CheckDatabaseServerVersion.php @@ -0,0 +1,70 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use LibreNMS\DB\Eloquent; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Util\Version; +use LibreNMS\ValidationResult; +use LibreNMS\Validations\Database; + +class CheckDatabaseServerVersion implements Validation +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + $version = Version::get()->databaseServer(); + $version = explode('-', $version); + + if (isset($version[1]) && $version[1] == 'MariaDB') { + if (version_compare($version[0], Database::MARIADB_MIN_VERSION, '<=')) { + return ValidationResult::fail( + trans('validation.validations.database.CheckDatabaseServerVersion.fail', ['server' => 'MariaDB', 'min' => Database::MARIADB_MIN_VERSION, 'date' => Database::MARIADB_MIN_VERSION_DATE]), + trans('validation.validations.database.CheckDatabaseServerVersion.fix', ['server' => 'MariaDB', 'suggested' => Database::MARIADB_RECOMMENDED_VERSION]), + ); + } + } else { + if (version_compare($version[0], Database::MYSQL_MIN_VERSION, '<=')) { + return ValidationResult::fail( + trans('validation.validations.database.CheckDatabaseServerVersion.fail', ['server' => 'MySQL', 'min' => Database::MYSQL_MIN_VERSION, 'date' => Database::MYSQL_MIN_VERSION_DATE]), + trans('validation.validations.database.CheckDatabaseServerVersion.fix', ['server' => 'MySQL', 'suggested' => Database::MYSQL_RECOMMENDED_VERSION]), + ); + } + } + + return ValidationResult::ok(trans('validation.validations.database.CheckDatabaseServerVersion.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected(); + } +} diff --git a/LibreNMS/Validations/Database/CheckDatabaseTableNamesCase.php b/LibreNMS/Validations/Database/CheckDatabaseTableNamesCase.php new file mode 100644 index 0000000000..407d27637f --- /dev/null +++ b/LibreNMS/Validations/Database/CheckDatabaseTableNamesCase.php @@ -0,0 +1,59 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use DB; +use LibreNMS\DB\Eloquent; +use LibreNMS\Interfaces\Validation; +use LibreNMS\ValidationResult; + +class CheckDatabaseTableNamesCase implements Validation +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + // Test for lower case table name support + $lc_mode = DB::selectOne('SELECT @@global.lower_case_table_names as mode')->mode; + if ($lc_mode != 0) { + ValidationResult::fail( + trans('validation.validations.database.CheckDatabaseTableNamesCase.fail'), + trans('validation.validations.database.CheckDatabaseTableNamesCase.fix') + ); + } + + return ValidationResult::ok(trans('validation.validations.database.CheckDatabaseTableNamesCase.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected(); + } +} diff --git a/LibreNMS/Validations/Database/CheckMysqlEngine.php b/LibreNMS/Validations/Database/CheckMysqlEngine.php new file mode 100644 index 0000000000..256020d79b --- /dev/null +++ b/LibreNMS/Validations/Database/CheckMysqlEngine.php @@ -0,0 +1,95 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use DB; +use Illuminate\Database\QueryException; +use Illuminate\Support\Collection; +use LibreNMS\DB\Eloquent; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Interfaces\ValidationFixer; +use LibreNMS\ValidationResult; + +class CheckMysqlEngine implements Validation, ValidationFixer +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + $tables = $this->findNonInnodbTables(); + + if ($tables->isNotEmpty()) { + return ValidationResult::warn(trans('validation.validations.database.CheckMysqlEngine.fail')) + ->setFixer(__CLASS__) + ->setList(trans('validation.validations.database.CheckMysqlEngine.tables'), $tables->all()); + } + + return ValidationResult::ok(trans('validation.validations.database.CheckMysqlEngine.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected(); + } + + /** + * @inheritDoc + */ + public function fix(): bool + { + try { + $db = $this->databaseName(); + $tables = $this->findNonInnodbTables(); + + foreach ($tables as $table) { + DB::statement("ALTER TABLE $db.$table ENGINE=InnoDB;"); + } + } catch (QueryException $e) { + return false; + } + + return true; + } + + private function databaseName(): string + { + return \config('database.connections.' . \config('database.default') . '.database'); + } + + private function findNonInnodbTables(): Collection + { + $db = $this->databaseName(); + + return DB::table('information_schema.tables') + ->where('TABLE_SCHEMA', $db) + ->where('ENGINE', '!=', 'InnoDB') + ->pluck('TABLE_NAME'); + } +} diff --git a/LibreNMS/Validations/Database/CheckSchemaCollation.php b/LibreNMS/Validations/Database/CheckSchemaCollation.php new file mode 100644 index 0000000000..775af0e57c --- /dev/null +++ b/LibreNMS/Validations/Database/CheckSchemaCollation.php @@ -0,0 +1,100 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use LibreNMS\DB\Eloquent; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Interfaces\ValidationFixer; +use LibreNMS\ValidationResult; + +class CheckSchemaCollation implements Validation, ValidationFixer +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + $db_name = Eloquent::DB()->selectOne('SELECT DATABASE() as name')->name; + + // Test for correct character set and collation + $db_collation_sql = "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME + FROM information_schema.SCHEMATA S + WHERE schema_name = '$db_name' AND + ( DEFAULT_CHARACTER_SET_NAME != 'utf8mb4' OR DEFAULT_COLLATION_NAME != 'utf8mb4_unicode_ci')"; + $collation = Eloquent::DB()->selectOne($db_collation_sql); + if (empty($collation) !== true) { + return ValidationResult::fail( + "MySQL Database collation is wrong: $collation->DEFAULT_CHARACTER_SET_NAME $collation->DEFAULT_COLLATION_NAME", + 'Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.' + )->setFixer(__CLASS__); + } + + $table_collation_sql = "SELECT T.TABLE_NAME, C.CHARACTER_SET_NAME, C.COLLATION_NAME + FROM information_schema.TABLES AS T, information_schema.COLLATION_CHARACTER_SET_APPLICABILITY AS C + WHERE C.collation_name = T.table_collation AND T.table_schema = '$db_name' AND + ( C.CHARACTER_SET_NAME != 'utf8mb4' OR C.COLLATION_NAME != 'utf8mb4_unicode_ci' );"; + $collation_tables = Eloquent::DB()->select($table_collation_sql); + if (empty($collation_tables) !== true) { + return ValidationResult::fail('MySQL tables collation is wrong: ') + ->setFix('Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.') + ->setFixer(__CLASS__) + ->setList('Tables', array_map(function ($row) { + return "$row->TABLE_NAME $row->CHARACTER_SET_NAME $row->COLLATION_NAME"; + }, $collation_tables)); + } + + $column_collation_sql = "SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME + FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$db_name' AND + ( CHARACTER_SET_NAME != 'utf8mb4' OR COLLATION_NAME != 'utf8mb4_unicode_ci' );"; + $collation_columns = Eloquent::DB()->select($column_collation_sql); + if (empty($collation_columns) !== true) { + return ValidationResult::fail('MySQL column collation is wrong: ') + ->setFix('Check https://community.librenms.org/t/new-default-database-charset-collation/14956 for info on how to fix.') + ->setFixer(__CLASS__) + ->setList('Columns', array_map(function ($row) { + return "$row->TABLE_NAME: $row->COLUMN_NAME $row->CHARACTER_SET_NAME $row->COLLATION_NAME"; + }, $collation_columns)); + } + + return ValidationResult::ok(''); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected() && CheckDatabaseSchemaVersion::isCurrent(); + } + + public function fix(): bool + { + \DB::table('migrations')->where('migration', '2021_02_09_122930_migrate_to_utf8mb4')->delete(); + $res = \Artisan::call('migrate', ['--force' => true]); + + return $res === 0; + } +} diff --git a/LibreNMS/Validations/Database/CheckSchemaStructure.php b/LibreNMS/Validations/Database/CheckSchemaStructure.php new file mode 100644 index 0000000000..982e0cd9a1 --- /dev/null +++ b/LibreNMS/Validations/Database/CheckSchemaStructure.php @@ -0,0 +1,329 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use DB; +use Illuminate\Database\QueryException; +use LibreNMS\Config; +use LibreNMS\DB\Eloquent; +use LibreNMS\DB\Schema; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Interfaces\ValidationFixer; +use LibreNMS\ValidationResult; +use Symfony\Component\Yaml\Yaml; + +class CheckSchemaStructure implements Validation, ValidationFixer +{ + /** @var array */ + private $descriptions = []; + /** @var array */ + private $schema_update = []; + /** @var string */ + private $schema_file; + + public function __construct() + { + $this->schema_file = Config::get('install_dir') . '/misc/db_schema.yaml'; + } + + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + if (! is_file($this->schema_file)) { + return ValidationResult::warn("We haven't detected the db_schema.yaml file"); + } + + $this->checkSchema(); + + if (empty($this->schema_update)) { + return ValidationResult::ok('Database schema correct'); + } + + return ValidationResult::fail("We have detected that your database schema may be wrong\n" . implode("\n", $this->descriptions)) + ->setFix('Run the following SQL statements to fix it') + ->setFixer(__CLASS__) + ->setList('SQL Statements', $this->schema_update); + } + + public function fix(): bool + { + try { + $this->checkSchema(); + + foreach ($this->schema_update as $query) { + DB::statement($query); + } + } catch (QueryException $e) { + return false; + } + + return true; + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected() && CheckDatabaseSchemaVersion::isCurrent(); + } + + private function checkSchema(): void + { + $master_schema = Yaml::parse(file_get_contents($this->schema_file)); + $current_schema = Schema::dump(); + + foreach ((array) $master_schema as $table => $data) { + if (empty($current_schema[$table])) { + $this->descriptions[] = "Database: missing table ($table)"; + $this->schema_update[] = $this->addTableSql($table, $data); + } else { + $current_columns = array_reduce($current_schema[$table]['Columns'], function ($array, $item) { + $array[$item['Field']] = $item; + + return $array; + }, []); + + foreach ($data['Columns'] as $index => $cdata) { + $column = $cdata['Field']; + + // MySQL 8 fix, remove DEFAULT_GENERATED from timestamp extra columns + if ($cdata['Type'] == 'timestamp') { + $current_columns[$column]['Extra'] = preg_replace('/DEFAULT_GENERATED */', '', $current_columns[$column]['Extra']); + } + + if (empty($current_columns[$column])) { + $this->descriptions[] = "Database: missing column ($table/$column)"; + $primary = false; + if ($data['Indexes']['PRIMARY']['Columns'] == [$column]) { + // include the primary index with the add statement + unset($data['Indexes']['PRIMARY']); + $primary = true; + } + $this->schema_update[] = $this->addColumnSql($table, $cdata, isset($data['Columns'][$index - 1]) ? $data['Columns'][$index - 1]['Field'] : null, $primary); + } elseif ($cdata !== $current_columns[$column]) { + $this->descriptions[] = "Database: incorrect column ($table/$column)"; + $this->schema_update[] = $this->updateTableSql($table, $column, $cdata); + } + + unset($current_columns[$column]); // remove checked columns + } + + foreach ($current_columns as $column => $_unused) { + $this->descriptions[] = "Database: extra column ($table/$column)"; + $this->schema_update[] = $this->dropColumnSql($table, $column); + } + + $index_changes = []; + if (isset($data['Indexes'])) { + foreach ($data['Indexes'] as $name => $index) { + if (empty($current_schema[$table]['Indexes'][$name])) { + $this->descriptions[] = "Database: missing index ($table/$name)"; + $index_changes[] = $this->addIndexSql($table, $index); + } elseif ($index != $current_schema[$table]['Indexes'][$name]) { + $this->descriptions[] = "Database: incorrect index ($table/$name)"; + $index_changes[] = $this->updateIndexSql($table, $name, $index); + } + + unset($current_schema[$table]['Indexes'][$name]); + } + } + + if (isset($current_schema[$table]['Indexes'])) { + foreach ($current_schema[$table]['Indexes'] as $name => $_unused) { + $this->descriptions[] = "Database: extra index ($table/$name)"; + $this->schema_update[] = $this->dropIndexSql($table, $name); + } + } + $this->schema_update = array_merge($this->schema_update, $index_changes); // drop before create/update + + $constraint_changes = []; + if (isset($data['Constraints'])) { + foreach ($data['Constraints'] as $name => $constraint) { + if (empty($current_schema[$table]['Constraints'][$name])) { + $this->descriptions[] = "Database: missing constraint ($table/$name)"; + $constraint_changes[] = $this->addConstraintSql($table, $constraint); + } elseif ($constraint != $current_schema[$table]['Constraints'][$name]) { + $this->descriptions[] = "Database: incorrect constraint ($table/$name)"; + $constraint_changes[] = $this->dropConstraintSql($table, $name); + $constraint_changes[] = $this->addConstraintSql($table, $constraint); + } + + unset($current_schema[$table]['Constraints'][$name]); + } + } + + if (isset($current_schema[$table]['Constraints'])) { + foreach ($current_schema[$table]['Constraints'] as $name => $_unused) { + $this->descriptions[] = "Database: extra constraint ($table/$name)"; + $this->schema_update[] = $this->dropConstraintSql($table, $name); + } + } + $this->schema_update = array_merge($this->schema_update, $constraint_changes); // drop before create/update + } + + unset($current_schema[$table]); // remove checked tables + } + + foreach ($current_schema as $table => $data) { + $this->descriptions[] = "Database: extra table ($table)"; + $this->schema_update[] = $this->dropTableSql($table); + } + + // set utc timezone if timestamp issues + if (preg_grep('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', $this->schema_update)) { + array_unshift($this->schema_update, "SET TIME_ZONE='+00:00';"); + } + } + + private function addTableSql(string $table, array $table_schema): string + { + $columns = array_map([$this, 'columnToSql'], $table_schema['Columns']); + $indexes = array_map([$this, 'indexToSql'], $table_schema['Indexes'] ?? []); + + $def = implode(', ', array_merge(array_values($columns), array_values($indexes))); + + return "CREATE TABLE `$table` ($def);"; + } + + private function addColumnSql(string $table, array $schema, ?string $previous_column, bool $primary = false): string + { + $sql = "ALTER TABLE `$table` ADD " . $this->columnToSql($schema); + if ($primary) { + $sql .= ' PRIMARY KEY'; + } + if (empty($previous_column)) { + $sql .= ' FIRST'; + } else { + $sql .= " AFTER `$previous_column`"; + } + + return $sql . ';'; + } + + private function updateTableSql(string $table, string $column, array $column_schema): string + { + return "ALTER TABLE `$table` CHANGE `$column` " . $this->columnToSql($column_schema) . ';'; + } + + private function dropColumnSql(string $table, string $column): string + { + return "ALTER TABLE `$table` DROP `$column`;"; + } + + private function addIndexSql(string $table, array $index_schema): string + { + return "ALTER TABLE `$table` ADD " . $this->indexToSql($index_schema) . ';'; + } + + private function updateIndexSql(string $table, string $name, array $index_schema): string + { + return "ALTER TABLE `$table` DROP INDEX `$name`, " . $this->indexToSql($index_schema) . ';'; + } + + private function dropIndexSql(string $table, string $name): string + { + return "ALTER TABLE `$table` DROP INDEX `$name`;"; + } + + private function dropTableSql(string $table): string + { + return "DROP TABLE `$table`;"; + } + + /** + * Generate an SQL segment to create the column based on data from Schema::dump() + * + * @param array $column_data The array of data for the column + * @return string sql fragment, for example: "`ix_id` int(10) unsigned NOT NULL" + */ + private function columnToSql(array $column_data): string + { + $segments = ["`${column_data['Field']}`", $column_data['Type']]; + + $segments[] = $column_data['Null'] ? 'NULL' : 'NOT NULL'; + + if (isset($column_data['Default'])) { + if ($column_data['Default'] === 'CURRENT_TIMESTAMP') { + $segments[] = 'DEFAULT CURRENT_TIMESTAMP'; + } elseif ($column_data['Default'] == 'NULL') { + $segments[] = 'DEFAULT NULL'; + } else { + $segments[] = "DEFAULT '${column_data['Default']}'"; + } + } + + if ($column_data['Extra'] == 'on update current_timestamp()') { + $segments[] = 'on update CURRENT_TIMESTAMP'; + } else { + $segments[] = $column_data['Extra']; + } + + return implode(' ', $segments); + } + + /** + * Generate an SQL segment to create the index based on data from Schema::dump() + * + * @param array $index_data The array of data for the index + * @return string sql fragment, for example: "PRIMARY KEY (`device_id`)" + */ + private function indexToSql(array $index_data): string + { + if ($index_data['Name'] == 'PRIMARY') { + $index = 'PRIMARY KEY (%s)'; + } elseif ($index_data['Unique']) { + $index = "UNIQUE `{$index_data['Name']}` (%s)"; + } else { + $index = "INDEX `{$index_data['Name']}` (%s)"; + } + + $columns = implode(',', array_map(function ($col) { + return "`$col`"; + }, $index_data['Columns'])); + + return sprintf($index, $columns); + } + + private function addConstraintSql(string $table, array $constraint): string + { + $sql = "ALTER TABLE `$table` ADD CONSTRAINT `{$constraint['name']}` FOREIGN KEY (`{$constraint['foreign_key']}`) "; + $sql .= " REFERENCES `{$constraint['table']}` (`{$constraint['key']}`)"; + if (! empty($constraint['extra'])) { + $sql .= ' ' . $constraint['extra']; + } + $sql .= ';'; + + return $sql; + } + + private function dropConstraintSql(string $table, string $name): string + { + return "ALTER TABLE `$table` DROP FOREIGN KEY `$name`;"; + } +} diff --git a/LibreNMS/Validations/Database/CheckSqlServerTime.php b/LibreNMS/Validations/Database/CheckSqlServerTime.php new file mode 100644 index 0000000000..fac32adbbf --- /dev/null +++ b/LibreNMS/Validations/Database/CheckSqlServerTime.php @@ -0,0 +1,68 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Database; + +use Carbon\Carbon; +use Carbon\CarbonInterval; +use LibreNMS\DB\Eloquent; +use LibreNMS\Interfaces\Validation; +use LibreNMS\ValidationResult; + +class CheckSqlServerTime implements Validation +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + $raw_time = Eloquent::DB()->selectOne('SELECT NOW() as time')->time; + $db_time = new Carbon($raw_time); + $php_time = Carbon::now(); + + $diff = $db_time->diffAsCarbonInterval($php_time); + + if ($diff->compare(CarbonInterval::minute(1)) > 0) { + $message = "Time between this server and the mysql database is off\n Mysql time :mysql_time\n PHP time :php_time"; + $message .= ' Mysql time ' . $db_time->toDateTimeString() . PHP_EOL; + $message .= ' PHP time ' . $php_time->toDateTimeString() . PHP_EOL; + + return ValidationResult::fail(trans('validation.validations.database.CheckSqlServerTime.fail', [ + 'mysql_time' => $db_time->toDateTimeString(), + 'php_time' => $php_time->toDateTimeString(), + ])); + } + + return ValidationResult::ok(trans('validation.validations.database.CheckSqlServerTime.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return Eloquent::isConnected(); + } +} diff --git a/LibreNMS/Validations/Dependencies.php b/LibreNMS/Validations/Dependencies.php index bf4d580beb..ff7a3ad7e0 100644 --- a/LibreNMS/Validations/Dependencies.php +++ b/LibreNMS/Validations/Dependencies.php @@ -38,7 +38,7 @@ class Dependencies extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { if (EnvHelper::librenmsDocker()) { $validator->ok('Installed from the official Docker image; no Composer required'); diff --git a/LibreNMS/Validations/Disk.php b/LibreNMS/Validations/Disk.php index f0fd1d9337..84e7877b99 100644 --- a/LibreNMS/Validations/Disk.php +++ b/LibreNMS/Validations/Disk.php @@ -36,7 +36,7 @@ class Disk extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { // Disk space and permission checks $temp_dir = Config::get('temp_dir'); diff --git a/LibreNMS/Validations/DistributedPoller.php b/LibreNMS/Validations/DistributedPoller.php index c8a9a305c0..c38c978396 100644 --- a/LibreNMS/Validations/DistributedPoller.php +++ b/LibreNMS/Validations/DistributedPoller.php @@ -35,11 +35,12 @@ namespace LibreNMS\Validations; use App\Models\PollerCluster; use Carbon\Carbon; use LibreNMS\Config; +use LibreNMS\Validations\Rrd\CheckRrdcachedConnectivity; use LibreNMS\Validator; class DistributedPoller extends BaseValidation { - public function isDefault() + public function isDefault(): bool { // run by default if distributed polling is enabled return Config::get('distributed_poller'); @@ -51,7 +52,7 @@ class DistributedPoller extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { if (! Config::get('distributed_poller')) { $validator->fail('You have not enabled distributed_poller', 'lnms config:set distributed_poller true'); @@ -64,7 +65,7 @@ class DistributedPoller extends BaseValidation } elseif (! is_dir(Config::get('rrd_dir'))) { $validator->fail('You have not configured $config[\'rrd_dir\']'); } else { - Rrd::checkRrdcached($validator); + $validator->result((new CheckRrdcachedConnectivity)->validate(), $this->name); } if (PollerCluster::exists()) { @@ -82,7 +83,7 @@ class DistributedPoller extends BaseValidation $this->checkPythonWrapper($validator); } - private function checkDispatcherService(Validator $validator) + private function checkDispatcherService(Validator $validator): void { $driver = config('cache.default'); if ($driver != 'redis') { @@ -109,7 +110,7 @@ class DistributedPoller extends BaseValidation } } - private function checkPythonWrapper(Validator $validator) + private function checkPythonWrapper(Validator $validator): void { if (! Config::get('distributed_poller_memcached_host')) { $validator->fail('You have not configured $config[\'distributed_poller_memcached_host\']'); diff --git a/LibreNMS/Validations/Mail.php b/LibreNMS/Validations/Mail.php index 7d4526152e..3c68ad482c 100644 --- a/LibreNMS/Validations/Mail.php +++ b/LibreNMS/Validations/Mail.php @@ -38,7 +38,7 @@ class Mail extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { if (Config::get('alert.transports.mail') === true) { $run_test = 1; diff --git a/LibreNMS/Validations/Php.php b/LibreNMS/Validations/Php.php index d69c5c628c..aa2fae7751 100644 --- a/LibreNMS/Validations/Php.php +++ b/LibreNMS/Validations/Php.php @@ -40,7 +40,7 @@ class Php extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { $this->checkVersion($validator); $this->checkExtensions($validator); diff --git a/LibreNMS/Validations/Programs.php b/LibreNMS/Validations/Programs.php index 5c2aeed845..e164911642 100644 --- a/LibreNMS/Validations/Programs.php +++ b/LibreNMS/Validations/Programs.php @@ -37,7 +37,7 @@ class Programs extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { // Check programs $bins = ['fping', 'rrdtool', 'snmpwalk', 'snmpget', 'snmpgetnext', 'snmpbulkwalk']; diff --git a/LibreNMS/Validations/Python.php b/LibreNMS/Validations/Python.php index 547691c8a6..7a714bc3bc 100644 --- a/LibreNMS/Validations/Python.php +++ b/LibreNMS/Validations/Python.php @@ -40,7 +40,7 @@ class Python extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { $version = Version::get()->python(); diff --git a/LibreNMS/Validations/Rrd.php b/LibreNMS/Validations/Rrd.php index f73cbce429..5c4dfdf3f3 100644 --- a/LibreNMS/Validations/Rrd.php +++ b/LibreNMS/Validations/Rrd.php @@ -25,65 +25,8 @@ namespace LibreNMS\Validations; -use LibreNMS\Config; -use LibreNMS\Util\Version; -use LibreNMS\Validator; - class Rrd extends BaseValidation { - /** - * Validate this module. - * To return ValidationResults, call ok, warn, fail, or result methods on the $validator - * - * @param Validator $validator - */ - public function validate(Validator $validator) - { - // Check that rrdtool config version is what we see - if (Config::has('rrdtool_version')) { - $rrd_version = Version::get()->rrdtool(); - if (version_compare(Config::get('rrdtool_version'), '1.5.5', '<') - && version_compare(Config::get('rrdtool_version'), $rrd_version, '>') - ) { - $validator->fail( - 'The rrdtool version you have specified is newer than what is installed.', - "Either comment out \$config['rrdtool_version'] = '" . - Config::get('rrdtool_version') . "'; or set \$config['rrdtool_version'] = '{$rrd_version}';" - ); - } - } - - if (Config::get('rrdcached')) { - self::checkRrdcached($validator); - } else { - $rrd_dir = Config::get('rrd_dir'); - - $dir_stat = stat($rrd_dir); - if ($dir_stat[4] == 0 || $dir_stat[5] == 0) { - $validator->warn('Your RRD directory is owned by root, please consider changing over to user a non-root user'); - } - - if (substr(sprintf('%o', fileperms($rrd_dir)), -3) != 775) { - $validator->warn('Your RRD directory is not set to 0775', "chmod 775 $rrd_dir"); - } - } - } - - public static function checkRrdcached(Validator $validator) - { - [$host,$port] = explode(':', Config::get('rrdcached')); - if ($host == 'unix') { - // Using socket, check that file exists - if (! file_exists($port)) { - $validator->fail("$port doesn't appear to exist, rrdcached test failed"); - } - } else { - $connection = @fsockopen($host, (int) $port); - if (is_resource($connection)) { - fclose($connection); - } else { - $validator->fail('Cannot connect to rrdcached instance'); - } - } - } + protected $directory = 'Rrd'; + protected $name = 'rrd'; } diff --git a/LibreNMS/Validations/Rrd/CheckRrdDirPermissions.php b/LibreNMS/Validations/Rrd/CheckRrdDirPermissions.php new file mode 100644 index 0000000000..35362acb8b --- /dev/null +++ b/LibreNMS/Validations/Rrd/CheckRrdDirPermissions.php @@ -0,0 +1,62 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Rrd; + +use LibreNMS\Config; +use LibreNMS\Interfaces\Validation; +use LibreNMS\ValidationResult; + +class CheckRrdDirPermissions implements Validation +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + $rrd_dir = Config::get('rrd_dir'); + + $dir_stat = stat($rrd_dir); + if ($dir_stat[4] == 0 || $dir_stat[5] == 0) { + return ValidationResult::warn(trans('validation.validations.rrd.CheckRrdDirPermissions.fail_root'), + sprintf('chown %s:%s %s', Config::get('user'), Config::get('group'), $rrd_dir) + ); + } + + if (substr(sprintf('%o', fileperms($rrd_dir)), -3) != 775) { + return ValidationResult::warn(trans('validation.validations.rrd.CheckRrdDirPermissions.fail_mode'), "chmod 775 $rrd_dir"); + } + + return ValidationResult::ok(trans('validation.validations.rrd.CheckRrdDirPermissions.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return ! Config::get('rrdcached'); + } +} diff --git a/LibreNMS/Validations/Rrd/CheckRrdVersion.php b/LibreNMS/Validations/Rrd/CheckRrdVersion.php new file mode 100644 index 0000000000..53d001241a --- /dev/null +++ b/LibreNMS/Validations/Rrd/CheckRrdVersion.php @@ -0,0 +1,73 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Rrd; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Support\Str; +use LibreNMS\Config; +use LibreNMS\Interfaces\Validation; +use LibreNMS\Interfaces\ValidationFixer; +use LibreNMS\ValidationResult; +use Storage; + +class CheckRrdVersion implements Validation, ValidationFixer +{ + public function validate(): ValidationResult + { + // Check that rrdtool config version is what we see + $rrd_version = '0.1'; //Version::get()->rrdtool(); + if (version_compare(Config::get('rrdtool_version'), '1.5.5', '<') + && version_compare(Config::get('rrdtool_version'), $rrd_version, '>') + ) { + return ValidationResult::fail( + trans('validation.validations.rrd.CheckRrdVersion.fail'), + trans('validation.validations.rrd.CheckRrdVersion.fix', ['version' => Config::get('rrdtool_version')]) + )->setFixer(__CLASS__, is_writable(base_path('config.php'))); + } + + return ValidationResult::ok(trans('validation.validations.rrd.CheckRrdVersion.ok')); + } + + public function enabled(): bool + { + return Config::has('rrdtool_version'); + } + + public function fix(): bool + { + try { + $contents = Storage::disk('base')->get('config.php'); + + $lines = array_filter(explode("\n", $contents), function ($line) { + return ! Str::contains($line, ['$config[\'rrdtool_version\']', '$config["rrdtool_version"]']); + }); + + return Storage::disk('base')->put('config.php', implode("\n", $lines)); + } catch (FileNotFoundException $e) { + return false; + } + } +} diff --git a/LibreNMS/Validations/Rrd/CheckRrdcachedConnectivity.php b/LibreNMS/Validations/Rrd/CheckRrdcachedConnectivity.php new file mode 100644 index 0000000000..856985c416 --- /dev/null +++ b/LibreNMS/Validations/Rrd/CheckRrdcachedConnectivity.php @@ -0,0 +1,64 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Validations\Rrd; + +use LibreNMS\Config; +use LibreNMS\Interfaces\Validation; +use LibreNMS\ValidationResult; + +class CheckRrdcachedConnectivity implements Validation +{ + /** + * @inheritDoc + */ + public function validate(): ValidationResult + { + [$host,$port] = explode(':', Config::get('rrdcached')); + if ($host == 'unix') { + // Using socket, check that file exists + if (! file_exists($port)) { + return ValidationResult::fail(trans('validation.validations.rrd.CheckRrdcachedConnectivity.fail_socket', ['socket' => $port])); + } + } else { + $connection = @fsockopen($host, (int) $port); + if (is_resource($connection)) { + fclose($connection); + } else { + return ValidationResult::fail(trans('validation.validations.rrd.CheckRrdcachedConnectivity.fail_port', ['port' => $port])); + } + } + + return ValidationResult::ok(trans('validation.validations.rrd.CheckRrdcachedConnectivity.ok')); + } + + /** + * @inheritDoc + */ + public function enabled(): bool + { + return (bool) Config::get('rrdcached'); + } +} diff --git a/LibreNMS/Validations/RrdCheck.php b/LibreNMS/Validations/RrdCheck.php index ef2fc8da94..68b2578bbd 100644 --- a/LibreNMS/Validations/RrdCheck.php +++ b/LibreNMS/Validations/RrdCheck.php @@ -41,7 +41,7 @@ class RrdCheck extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { // Loop through the rrd_dir $rrd_directory = new RecursiveDirectoryIterator(Config::get('rrd_dir')); diff --git a/LibreNMS/Validations/System.php b/LibreNMS/Validations/System.php index 8324d87fea..bed5146deb 100644 --- a/LibreNMS/Validations/System.php +++ b/LibreNMS/Validations/System.php @@ -36,7 +36,7 @@ class System extends BaseValidation /** * {@inheritdoc} */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { $install_dir = $validator->getBaseDir(); diff --git a/LibreNMS/Validations/Updates.php b/LibreNMS/Validations/Updates.php index 25744b269d..ff1835b83f 100644 --- a/LibreNMS/Validations/Updates.php +++ b/LibreNMS/Validations/Updates.php @@ -38,7 +38,7 @@ use LibreNMS\Validator; class Updates extends BaseValidation { - public function validate(Validator $validator) + public function validate(Validator $validator): void { if (EnvHelper::librenmsDocker()) { $validator->warn('Updates are managed through the official Docker image'); diff --git a/LibreNMS/Validations/User.php b/LibreNMS/Validations/User.php index f46085b0d0..4ff388562b 100644 --- a/LibreNMS/Validations/User.php +++ b/LibreNMS/Validations/User.php @@ -41,7 +41,7 @@ class User extends BaseValidation * * @param Validator $validator */ - public function validate(Validator $validator) + public function validate(Validator $validator): void { // Check we are running this as the root user $username = $validator->getUsername(); diff --git a/LibreNMS/Validator.php b/LibreNMS/Validator.php index e592ade2c1..83ea7c0571 100644 --- a/LibreNMS/Validator.php +++ b/LibreNMS/Validator.php @@ -154,6 +154,17 @@ class Validator foreach ($results as $result) { $result->consolePrint(); + if ($result->hasFixer()) { + $input = readline('Attempt to fix this issue (y or n)?:'); + if ($input === 'y') { + $result = app()->make($result->getFixer())->fix(); + if ($result) { + echo "Attempted to apply fix.\n"; + } else { + echo "Failed to apply fix.\n"; + } + } + } } } diff --git a/app/Http/Controllers/ValidateController.php b/app/Http/Controllers/ValidateController.php index dd95121ca8..b4c8c863e6 100644 --- a/app/Http/Controllers/ValidateController.php +++ b/app/Http/Controllers/ValidateController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers; use Illuminate\Contracts\View\View; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use LibreNMS\Interfaces\ValidationFixer; use LibreNMS\Validator; class ValidateController extends Controller @@ -20,4 +22,23 @@ class ValidateController extends Controller return response()->json($validator->toArray()); } + + public function runFixer(Request $request): JsonResponse + { + $this->validate($request, [ + 'fixer' => [ + 'starts_with:LibreNMS\Validations', + function ($attribute, $value, $fail) { + if (! class_exists($value) || ! in_array(ValidationFixer::class, class_implements($value))) { + $fail(trans('validation.results.invalid_fixer')); + } + }, + ], + ]); + $fixer = $request->get('fixer'); + + return response()->json([ + 'result' => (new $fixer)->fix(), + ]); + } } diff --git a/config/filesystems.php b/config/filesystems.php index db917c4461..31634535df 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -43,6 +43,11 @@ return [ 'root' => storage_path('app'), ], + 'base' => [ + 'driver' => 'local', + 'root' => base_path(), + ], + 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), diff --git a/database/seeders/ConfigSeeder.php b/database/seeders/ConfigSeeder.php index 2286316acd..9f6ea6c3c6 100644 --- a/database/seeders/ConfigSeeder.php +++ b/database/seeders/ConfigSeeder.php @@ -58,7 +58,7 @@ class ConfigSeeder extends Seeder } if (\App\Models\Config::exists()) { - if (! $this->command->confirm(trans('commands.db:seed.existing_config'), false)) { + if (! app()->runningInConsole() || ! $this->command->confirm(trans('commands.db:seed.existing_config'), false)) { return; // don't overwrite existing settings. } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 24effc0b4f..691b19f778 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3340,11 +3340,6 @@ parameters: count: 1 path: LibreNMS/Interfaces/Polling/SlaPolling.php - - - message: "#^Method LibreNMS\\\\Interfaces\\\\ValidationGroup\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Interfaces/ValidationGroup.php - - message: "#^Method LibreNMS\\\\Model\\:\\:clean\\(\\) has no return type specified\\.$#" count: 1 @@ -6540,266 +6535,6 @@ parameters: count: 1 path: LibreNMS/Util/Validate.php - - - message: "#^Property LibreNMS\\\\Validations\\\\BaseValidation\\:\\:\\$RUN_BY_DEFAULT has no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/BaseValidation.php - - - - message: "#^Property LibreNMS\\\\Validations\\\\BaseValidation\\:\\:\\$completed has no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/BaseValidation.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Configuration\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Configuration.php - - - - message: "#^Comparison operation \"\\>\" between int\\<1001, max\\> and 1000 is always true\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addColumnSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addColumnSql\\(\\) has parameter \\$previous_column with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addColumnSql\\(\\) has parameter \\$primary with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addColumnSql\\(\\) has parameter \\$schema with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addColumnSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addConstraintSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addConstraintSql\\(\\) has parameter \\$constraint with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addConstraintSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addIndexSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addIndexSql\\(\\) has parameter \\$index_schema with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addIndexSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addTableSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addTableSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:addTableSql\\(\\) has parameter \\$table_schema with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkCollation\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkMode\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkMysqlEngine\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkSchema\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkTime\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:checkVersion\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropColumnSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropColumnSql\\(\\) has parameter \\$column with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropColumnSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropConstraintSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropConstraintSql\\(\\) has parameter \\$name with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropConstraintSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropIndexSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropIndexSql\\(\\) has parameter \\$name with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropIndexSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropTableSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:dropTableSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateIndexSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateIndexSql\\(\\) has parameter \\$index_schema with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateIndexSql\\(\\) has parameter \\$name with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateIndexSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateTableSql\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateTableSql\\(\\) has parameter \\$column with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateTableSql\\(\\) has parameter \\$column_schema with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:updateTableSql\\(\\) has parameter \\$table with no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Database\\:\\:validateSystem\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Database.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Dependencies\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Dependencies.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Disk\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Disk.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\DistributedPoller\\:\\:checkDispatcherService\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/DistributedPoller.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\DistributedPoller\\:\\:checkPythonWrapper\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/DistributedPoller.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\DistributedPoller\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/DistributedPoller.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Mail\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Mail.php - - - - message: "#^Property LibreNMS\\\\Validations\\\\Mail\\:\\:\\$RUN_BY_DEFAULT has no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Mail.php - - message: "#^Method LibreNMS\\\\Validations\\\\Php\\:\\:checkExtensions\\(\\) has no return type specified\\.$#" count: 1 @@ -6820,11 +6555,6 @@ parameters: count: 1 path: LibreNMS/Validations/Php.php - - - message: "#^Method LibreNMS\\\\Validations\\\\Php\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Php.php - - message: "#^Method LibreNMS\\\\Validations\\\\Programs\\:\\:checkFping6\\(\\) has no return type specified\\.$#" count: 1 @@ -6875,11 +6605,6 @@ parameters: count: 1 path: LibreNMS/Validations/Programs.php - - - message: "#^Method LibreNMS\\\\Validations\\\\Programs\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Programs.php - - message: "#^Method LibreNMS\\\\Validations\\\\Python\\:\\:checkExtensions\\(\\) has no return type specified\\.$#" count: 1 @@ -6905,51 +6630,6 @@ parameters: count: 1 path: LibreNMS/Validations/Python.php - - - message: "#^Method LibreNMS\\\\Validations\\\\Python\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Python.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Rrd\\:\\:checkRrdcached\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Rrd.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Rrd\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Rrd.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\RrdCheck\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/RrdCheck.php - - - - message: "#^Property LibreNMS\\\\Validations\\\\RrdCheck\\:\\:\\$RUN_BY_DEFAULT has no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/RrdCheck.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\System\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/System.php - - - - message: "#^Property LibreNMS\\\\Validations\\\\System\\:\\:\\$RUN_BY_DEFAULT has no type specified\\.$#" - count: 1 - path: LibreNMS/Validations/System.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\Updates\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/Updates.php - - - - message: "#^Method LibreNMS\\\\Validations\\\\User\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: LibreNMS/Validations/User.php - - message: "#^Property App\\\\Models\\\\Device\\:\\:\\$authlevel \\('authNoPriv'\\|'authPriv'\\|'noAuthNoPriv'\\) does not accept null\\.$#" count: 1 diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 6dd760390f..d614648b24 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -160,12 +160,66 @@ return [ 'attributes' => [], 'results' => [ + 'autofix' => 'Attempt to automatically fix', 'fix' => 'Fix', + 'fixed' => 'Fix has completed, refresh to re-run validations.', 'fetch_failed' => 'Failed to fetch validation results', 'backend_failed' => 'Failed to load data from backend, check webserver.', + 'invalid_fixer' => 'Invalid Fixer', 'show_all' => 'Show all', 'show_less' => 'Show less', 'validate' => 'Validate', 'validating' => 'Validating', ], + 'validations' => [ + 'rrd' => [ + 'CheckRrdVersion' => [ + 'fail' => 'The rrdtool version you have specified is newer than what is installed.', + 'fix' => 'Either comment out or delete $config[\'rrdtool_version\'] = \':version\'; from your config.php file', + 'ok' => 'rrdtool version ok', + ], + 'CheckRrdcachedConnectivity' => [ + 'fail_socket' => ':socket does not appear to exist, rrdcached connectivity test failed', + 'fail_port' => 'Cannot connect to rrdcached server on port :port', + 'ok' => 'Connected to rrdcached', + ], + 'CheckRrdDirPermissions' => [ + 'fail_root' => 'Your RRD directory is owned by root, please consider changing over to user a non-root user', + 'fail_mode' => 'Your RRD directory is not set to 0775', + 'ok' => 'rrd_dir is writable', + ], + ], + 'database' => [ + 'CheckDatabaseTableNamesCase' => [ + 'fail' => 'You have lower_case_table_names set to 1 or true in mysql config.', + 'fix' => 'Set lower_case_table_names=0 in your mysql config file in the [mysqld] section.', + 'ok' => 'lower_case_table_names is enabled', + ], + 'CheckDatabaseServerVersion' => [ + 'fail' => ':server version :min is the minimum supported version as of :date.', + 'fix' => 'Update :server to a supported version, :suggested suggested.', + 'ok' => 'SQL Server meets minimum requirements', + ], + 'CheckMysqlEngine' => [ + 'fail' => 'Some tables are not using the recommended InnoDB engine, this may cause you issues.', + 'tables' => 'Tables', + 'ok' => 'MySQL engine is optimal', + ], + 'CheckSqlServerTime' => [ + 'fail' => "Time between this server and the mysql database is off\n Mysql time :mysql_time\n PHP time :php_time", + 'ok' => 'MySQl and PHP time match', + ], + 'CheckSchemaVersion' => [ + 'fail_outdated' => 'Your database is out of date!', + 'fail_legacy_outdated' => 'Your database schema (:current) is older than the latest (:latest).', + 'fix_legacy_outdated' => 'Manually run ./daily.sh, and check for any errors.', + 'warn_extra_migrations' => 'Your database schema has extra migrations (:migrations). If you just switched to the stable release from the daily release, your database is in between releases and this will be resolved with the next release.', + 'warn_legacy_newer' => 'Your database schema (:current) is newer than expected (:latest). If you just switched to the stable release from the daily release, your database is in between releases and this will be resolved with the next release.', + 'ok' => 'Database Schema is current', + ], + 'CheckSchemaCollation' => [ + + ], + ], + ], ]; diff --git a/resources/views/validate/index.blade.php b/resources/views/validate/index.blade.php index caaa499ead..0b4ae30a93 100644 --- a/resources/views/validate/index.blade.php +++ b/resources/views/validate/index.blade.php @@ -36,7 +36,13 @@
-
+
+
+ +
{{ __('validation.results.fixed') }}
+
{{ __('validation.results.fix') }}:
                                                 
@@ -71,6 +77,30 @@ @push('scripts') @endpush diff --git a/routes/web.php b/routes/web.php index 97cacde16f..d6a0e5a06f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -98,6 +98,7 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () { Route::resource('port-groups', 'PortGroupController'); Route::get('validate', [\App\Http\Controllers\ValidateController::class, 'index'])->name('validate'); Route::get('validate/results', [\App\Http\Controllers\ValidateController::class, 'runValidation'])->name('validate.results'); + Route::post('validate/fix', [\App\Http\Controllers\ValidateController::class, 'runFixer'])->name('validate.fix'); }); Route::get('plugin', 'PluginLegacyController@redirect'); diff --git a/tests/Unit/ValidationFixTest.php b/tests/Unit/ValidationFixTest.php new file mode 100644 index 0000000000..2d276a0fd5 --- /dev/null +++ b/tests/Unit/ValidationFixTest.php @@ -0,0 +1,58 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Tests\Unit; + +use LibreNMS\Tests\TestCase; +use LibreNMS\Validations\Rrd\CheckRrdVersion; +use Storage; + +class ValidationFixTest extends TestCase +{ + public function testRrdVersionFix(): void + { + Storage::fake('base'); + Storage::disk('base')->put('config.php', <<<'EOF' +fix(); + + $actual = Storage::disk('base')->get('config.php'); + $this->assertSame(<<<'EOF' +delete('config.php'); + } +} diff --git a/validate.php b/validate.php index 2fb99e55dd..38d15d9a92 100755 --- a/validate.php +++ b/validate.php @@ -172,13 +172,13 @@ EOF; // output matches that of ValidationResult function print_fail($msg, $fix = null) { - c_echo("[%RFAIL%n] $msg"); + echo "[\033[31;1mFAIL\033[0m] $msg"; if ($fix && strlen($msg) > 72) { echo PHP_EOL . ' '; } if (! empty($fix)) { - c_echo(" [%BFIX%n] %B$fix%n"); + echo " [\033[34;1mFIX\033[0m] \033[34;1m$fix\033[0m"; } echo PHP_EOL; }