2017-10-26 01:56:09 -05:00
< ? php
/**
* Database . php
*
* Checks the database for errors
*
* 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 2017 Tony Murray
* @ author Tony Murray < murraytony @ gmail . com >
*/
namespace LibreNMS\Validations ;
2018-10-23 15:56:09 -05:00
use Carbon\Carbon ;
use Carbon\CarbonInterval ;
2017-10-26 01:56:09 -05:00
use LibreNMS\Config ;
2018-10-23 15:56:09 -05:00
use LibreNMS\DB\Eloquent ;
2019-02-05 16:50:51 -06:00
use LibreNMS\DB\Schema ;
2017-10-26 01:56:09 -05:00
use LibreNMS\ValidationResult ;
use LibreNMS\Validator ;
use Symfony\Component\Yaml\Yaml ;
2018-02-27 09:57:20 -06:00
class Database extends BaseValidation
2017-10-26 01:56:09 -05:00
{
public function validate ( Validator $validator )
{
if ( ! dbIsConnected ()) {
return ;
}
$this -> checkMode ( $validator );
2018-10-23 15:56:09 -05:00
$this -> checkTime ( $validator );
2017-10-26 01:56:09 -05:00
// check database schema version
$current = get_db_schema ();
2019-01-14 07:44:23 -05:00
$latest = 1000 ;
2017-10-26 01:56:09 -05:00
2019-01-14 07:44:23 -05:00
if ( $current === 0 || $current === $latest ) {
2019-02-06 10:53:25 -06:00
// Using Laravel migrations
2019-02-05 16:50:51 -06:00
if ( ! Schema :: isCurrent ()) {
2019-01-18 11:39:10 -06:00
$validator -> fail ( " Your database is out of date! " , './lnms migrate' );
2019-01-14 07:44:23 -05:00
return ;
}
2019-02-05 16:50:51 -06:00
2019-02-06 10:53:25 -06:00
$migrations = Schema :: getUnexpectedMigrations ();
if ( $migrations -> isNotEmpty ()) {
2019-02-05 16:50:51 -06:00
$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. " );
}
2019-01-14 07:44:23 -05:00
} elseif ( $current < $latest ) {
2017-10-26 01:56:09 -05:00
$validator -> fail (
" Your database schema ( $current ) is older than the latest ( $latest ). " ,
" Manually run ./daily.sh, and check for any errors. "
);
return ;
} elseif ( $current > $latest ) {
2018-12-13 14:16:08 +01:00
$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. " );
2017-10-26 01:56:09 -05:00
}
2019-02-12 17:55:52 -06:00
$this -> checkMysqlEngine ( $validator );
2017-10-26 01:56:09 -05:00
$this -> checkCollation ( $validator );
$this -> checkSchema ( $validator );
}
2018-10-23 15:56:09 -05:00
private function checkTime ( Validator $validator )
{
$raw_time = Eloquent :: DB () -> selectOne ( Eloquent :: DB () -> raw ( '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 );
}
}
2017-10-26 01:56:09 -05:00
private function checkMode ( Validator $validator )
{
// Test for lower case table name support
$lc_mode = dbFetchCell ( " SELECT @@global.lower_case_table_names " );
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.'
);
}
}
2019-02-12 17:55:52 -06:00
private function checkMysqlEngine ( Validator $validator )
{
$db = Config :: get ( 'db_name' , 'librenms' );
$query = " SELECT `TABLE_NAME` FROM information_schema.tables WHERE `TABLE_SCHEMA` = ' $db ' && `ENGINE` != 'InnoDB' " ;
$tables = dbFetchRows ( $query );
if ( ! empty ( $tables )) {
$validator -> result (
ValidationResult :: warn ( " Some tables are not using the recommended InnoDB engine, this may cause you issues. " )
-> setList ( 'Tables' , $tables )
);
}
}
2017-10-26 01:56:09 -05:00
private function checkCollation ( Validator $validator )
{
2018-04-11 10:15:13 -05:00
$db_name = dbFetchCell ( 'SELECT DATABASE()' );
2017-10-26 01:56:09 -05:00
// Test for correct character set and collation
$db_collation_sql = " SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM information_schema . SCHEMATA S
2018-04-11 10:15:13 -05:00
WHERE schema_name = '$db_name' AND
( DEFAULT_CHARACTER_SET_NAME != 'utf8' OR DEFAULT_COLLATION_NAME != 'utf8_unicode_ci' ) " ;
2017-10-26 01:56:09 -05:00
$collation = dbFetchRows ( $db_collation_sql );
if ( empty ( $collation ) !== true ) {
$validator -> fail (
'MySQL Database collation is wrong: ' . implode ( ' ' , $collation [ 0 ]),
'Check https://t.libren.ms/-zdwk 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
2018-04-11 10:15:13 -05:00
WHERE C . collation_name = T . table_collation AND T . table_schema = '$db_name' AND
( C . CHARACTER_SET_NAME != 'utf8' OR C . COLLATION_NAME != 'utf8_unicode_ci' ); " ;
2017-10-26 01:56:09 -05:00
$collation_tables = dbFetchRows ( $table_collation_sql );
if ( empty ( $collation_tables ) !== true ) {
$result = ValidationResult :: fail ( 'MySQL tables collation is wrong: ' )
-> setFix ( 'Check http://bit.ly/2lAG9H8 for info on how to fix.' )
-> setList ( 'Tables' , $collation_tables );
$validator -> result ( $result );
}
$column_collation_sql = " SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
2018-04-11 10:15:13 -05:00
FROM information_schema . COLUMNS WHERE TABLE_SCHEMA = '$db_name' AND
( CHARACTER_SET_NAME != 'utf8' OR COLLATION_NAME != 'utf8_unicode_ci' ); " ;
2017-10-26 01:56:09 -05:00
$collation_columns = dbFetchRows ( $column_collation_sql );
if ( empty ( $collation_columns ) !== true ) {
$result = ValidationResult :: fail ( 'MySQL column collation is wrong: ' )
-> setFix ( 'Check https://t.libren.ms/-zdwk for info on how to fix.' )
-> setList ( 'Columns' , $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 = dump_db_schema ();
$schema_update = array ();
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 ;
}, array ());
foreach ( $data [ 'Columns' ] as $index => $cdata ) {
$column = $cdata [ 'Field' ];
2019-03-09 21:15:03 +01:00
// 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' ]);
}
2017-10-26 01:56:09 -05:00
if ( empty ( $current_columns [ $column ])) {
$validator -> fail ( " Database: missing column ( $table / $column ) " );
2018-03-23 02:51:34 -05:00
$primary = false ;
if ( $data [ 'Indexes' ][ 'PRIMARY' ][ 'Columns' ] == [ $column ]) {
// include the primary index with the add statement
unset ( $data [ 'Indexes' ][ 'PRIMARY' ]);
$primary = true ;
}
2019-01-17 08:58:26 -06:00
$schema_update [] = $this -> addColumnSql ( $table , $cdata , isset ( $data [ 'Columns' ][ $index - 1 ]) ? $data [ 'Columns' ][ $index - 1 ][ 'Field' ] : null , $primary );
2017-10-26 01:56:09 -05:00
} 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 );
}
2019-01-17 08:59:42 -06:00
$index_changes = [];
2017-10-26 01:56:09 -05:00
if ( isset ( $data [ 'Indexes' ])) {
foreach ( $data [ 'Indexes' ] as $name => $index ) {
if ( empty ( $current_schema [ $table ][ 'Indexes' ][ $name ])) {
$validator -> fail ( " Database: missing index ( $table / $name ) " );
2019-01-17 08:59:42 -06:00
$index_changes [] = $this -> addIndexSql ( $table , $index );
2017-10-26 01:56:09 -05:00
} elseif ( $index != $current_schema [ $table ][ 'Indexes' ][ $name ]) {
$validator -> fail ( " Database: incorrect index ( $table / $name ) " );
2019-01-17 08:59:42 -06:00
$index_changes [] = $this -> updateIndexSql ( $table , $name , $index );
2017-10-26 01:56:09 -05:00
}
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 );
}
}
2019-01-17 08:59:42 -06:00
$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
2017-10-26 01:56:09 -05:00
}
unset ( $current_schema [ $table ]); // remove checked tables
}
foreach ( $current_schema as $table => $data ) {
$validator -> fail ( " Database: extra table ( $table ) " );
$schema_update [] = $this -> dropTableSql ( $table );
}
if ( empty ( $schema_update )) {
$validator -> ok ( 'Database schema correct' );
} else {
2018-09-15 11:16:45 +02:00
$result = ValidationResult :: fail ( " We have detected that your database schema may be wrong, please report the following to us on Discord (https://t.libren.ms/discord) or the community site (https://t.libren.ms/5gscd): " )
2017-10-26 01:56:09 -05:00
-> setFix ( 'Run the following SQL statements to fix.' )
-> setList ( 'SQL Statements' , $schema_update );
$validator -> result ( $result );
}
}
private function addTableSql ( $table , $table_schema )
{
$columns = array_map ( array ( $this , 'columnToSql' ), $table_schema [ 'Columns' ]);
2019-01-17 08:58:26 -06:00
$indexes = array_map ( array ( $this , 'indexToSql' ), isset ( $table_schema [ 'Indexes' ]) ? $table_schema [ 'Indexes' ] : []);
2017-10-26 01:56:09 -05:00
2018-08-04 21:07:12 +01:00
$def = implode ( ', ' , array_merge ( array_values (( array ) $columns ), array_values (( array ) $indexes )));
2017-10-26 01:56:09 -05:00
return " CREATE TABLE ` $table ` ( $def ); " ;
}
2018-03-23 02:51:34 -05:00
private function addColumnSql ( $table , $schema , $previous_column , $primary = false )
2017-10-26 01:56:09 -05:00
{
$sql = " ALTER TABLE ` $table ` ADD " . $this -> columnToSql ( $schema );
2018-03-23 02:51:34 -05:00
if ( $primary ) {
$sql .= ' PRIMARY KEY' ;
}
if ( empty ( $previous_column )) {
$sql .= ' FIRST' ;
} else {
2017-10-26 01:56:09 -05:00
$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 `; " ;
}
/**
* Generate an SQL segment to create the column based on data from dump_db_schema ()
*
* @ 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 ( $column_data )
{
2018-03-23 02:51:34 -05:00
$segments = [ " ` ${ column_data['Field'] } ` " , $column_data [ 'Type' ]];
2017-10-26 01:56:09 -05:00
2018-03-23 02:51:34 -05:00
$segments [] = $column_data [ 'Null' ] ? 'NULL' : 'NOT NULL' ;
2017-10-26 01:56:09 -05:00
2018-03-23 02:51:34 -05:00
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' ;
2017-10-26 01:56:09 -05:00
} else {
2018-03-23 02:51:34 -05:00
$segments [] = $column_data [ 'Extra' ];
2017-10-26 01:56:09 -05:00
}
2018-03-23 02:51:34 -05:00
return implode ( ' ' , $segments );
2017-10-26 01:56:09 -05:00
}
/**
* Generate an SQL segment to create the index based on data from dump_db_schema ()
*
* @ 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 );
}
2019-01-17 08:59:42 -06:00
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 `; " ;
}
2017-10-26 01:56:09 -05:00
}