diff --git a/lib/Zonemaster/Backend/Config.pm b/lib/Zonemaster/Backend/Config.pm index 65066c34d..a129072b2 100644 --- a/lib/Zonemaster/Backend/Config.pm +++ b/lib/Zonemaster/Backend/Config.pm @@ -76,7 +76,20 @@ Emits a log warning with a deprecation message for each deprecated property that is present. Throws an exception if the given configuration contains errors. -Unrecognized sections and properties are silently ignored. + +In a valid config file: + +=over 4 + +=item + +all required properties are present, and + +=item + +all sections and properties are recognized. + +=back =cut @@ -88,9 +101,26 @@ sub parse { my $ini = Config::IniFiles->new( -file => \$text ) or die "Failed to parse config: " . join( '; ', @Config::IniFiles::errors ) . "\n"; + my $get_and_clear = sub { # Read and clear a property from a Config::IniFiles object. + my ( $section, $param ) = @_; + my $value = $ini->val( $section, $param ); + $ini->delval( $section, $param ); + return $value; + }; + + # Validate section names + { + my %sections = map { $_ => 1 } ( 'DB', 'MYSQL', 'POSTGRESQL', 'SQLITE', 'LANGUAGE', 'PUBLIC PROFILES', 'PRIVATE PROFILES', 'ZONEMASTER' ); + for my $section ( $ini->Sections ) { + if ( !exists $sections{$section} ) { + die "config: unrecognized section: $section\n"; + } + } + } + # Validate, normalize, and apply default values { - my $engine = $ini->val( 'DB', 'engine' ); + my $engine = $get_and_clear->( 'DB', 'engine' ); eval { $engine = $obj->check_db($engine); }; @@ -100,25 +130,25 @@ sub parse { $obj->{_DB_engine} = $engine; } - $obj->{_DB_polling_interval} = $ini->val( 'DB', 'polling_interval', '0.5' ); - $obj->{_MYSQL_host} = $ini->val( 'MYSQL', 'host', undef ); - $obj->{_MYSQL_user} = $ini->val( 'MYSQL', 'user', undef ); - $obj->{_MYSQL_password} = $ini->val( 'MYSQL', 'password', undef ); - $obj->{_MYSQL_database} = $ini->val( 'MYSQL', 'database', undef ); - $obj->{_POSTGRESQL_host} = $ini->val( 'POSTGRESQL', 'host', undef ); - $obj->{_POSTGRESQL_user} = $ini->val( 'POSTGRESQL', 'user', undef ); - $obj->{_POSTGRESQL_password} = $ini->val( 'POSTGRESQL', 'password', undef ); - $obj->{_POSTGRESQL_database} = $ini->val( 'POSTGRESQL', 'database', undef ); - $obj->{_SQLITE_database_file} = $ini->val( 'SQLITE', 'database_file', undef ); - $obj->{_ZONEMASTER_max_zonemaster_execution_time} = $ini->val( 'ZONEMASTER', 'max_zonemaster_execution_time', '600' ); - $obj->{_ZONEMASTER_maximal_number_of_retries} = $ini->val( 'ZONEMASTER', 'maximal_number_of_retries', '0' ); - $obj->{_ZONEMASTER_number_of_processes_for_frontend_testing} = $ini->val( 'ZONEMASTER', 'number_of_processes_for_frontend_testing', '20' ); - $obj->{_ZONEMASTER_number_of_processes_for_batch_testing} = $ini->val( 'ZONEMASTER', 'number_of_processes_for_batch_testing', '20' ); - $obj->{_ZONEMASTER_lock_on_queue} = $ini->val( 'ZONEMASTER', 'lock_on_queue', '0' ); - $obj->{_ZONEMASTER_age_reuse_previous_test} = $ini->val( 'ZONEMASTER', 'age_reuse_previous_test', '600' ); + $obj->{_DB_polling_interval} = $get_and_clear->( 'DB', 'polling_interval' ) // '0.5'; + $obj->{_MYSQL_host} = $get_and_clear->( 'MYSQL', 'host' ) // undef; + $obj->{_MYSQL_user} = $get_and_clear->( 'MYSQL', 'user' ) // undef; + $obj->{_MYSQL_password} = $get_and_clear->( 'MYSQL', 'password' ) // undef; + $obj->{_MYSQL_database} = $get_and_clear->( 'MYSQL', 'database' ) // undef; + $obj->{_POSTGRESQL_host} = $get_and_clear->( 'POSTGRESQL', 'host' ) // undef; + $obj->{_POSTGRESQL_user} = $get_and_clear->( 'POSTGRESQL', 'user' ) // undef; + $obj->{_POSTGRESQL_password} = $get_and_clear->( 'POSTGRESQL', 'password' ) // undef; + $obj->{_POSTGRESQL_database} = $get_and_clear->( 'POSTGRESQL', 'database' ) // undef; + $obj->{_SQLITE_database_file} = $get_and_clear->( 'SQLITE', 'database_file' ) // undef; + $obj->{_ZONEMASTER_max_zonemaster_execution_time} = $get_and_clear->( 'ZONEMASTER', 'max_zonemaster_execution_time' ) // '600'; + $obj->{_ZONEMASTER_maximal_number_of_retries} = $get_and_clear->( 'ZONEMASTER', 'maximal_number_of_retries' ) // '0'; + $obj->{_ZONEMASTER_number_of_processes_for_frontend_testing} = $get_and_clear->( 'ZONEMASTER', 'number_of_processes_for_frontend_testing' ) // '20'; + $obj->{_ZONEMASTER_number_of_processes_for_batch_testing} = $get_and_clear->( 'ZONEMASTER', 'number_of_processes_for_batch_testing' ) // '20'; + $obj->{_ZONEMASTER_lock_on_queue} = $get_and_clear->( 'ZONEMASTER', 'lock_on_queue' ) // '0'; + $obj->{_ZONEMASTER_age_reuse_previous_test} = $get_and_clear->( 'ZONEMASTER', 'age_reuse_previous_test' ) // '600'; $obj->{_LANGUAGE_locale} = {}; - for my $locale_tag ( split /\s+/, $ini->val( 'LANGUAGE', 'locale' ) || 'en_US' ) { + for my $locale_tag ( split /\s+/, $get_and_clear->( 'LANGUAGE', 'locale' ) || 'en_US' ) { $locale_tag =~ /^[a-z]{2}_[A-Z]{2}$/ or die "Illegal locale tag in LANGUAGE.locale: $locale_tag\n"; @@ -132,16 +162,22 @@ sub parse { default => '', }; for my $name ( $ini->Parameters( 'PUBLIC PROFILES' ) ) { - $obj->{_public_profiles}{lc $name} = $ini->val( 'PUBLIC PROFILES', $name ); + $obj->{_public_profiles}{lc $name} = $get_and_clear->( 'PUBLIC PROFILES', $name ); } $obj->{_private_profiles} = {}; for my $name ( $ini->Parameters( 'PRIVATE PROFILES' ) ) { - $obj->{_private_profiles}{lc $name} = $ini->val( 'PRIVATE PROFILES', $name ); + $obj->{_private_profiles}{lc $name} = $get_and_clear->( 'PRIVATE PROFILES', $name ); + } + + # Check required propertys (part 1/2) + if ( !defined $obj->DB_engine ) { + die "config: missing required property DB.engine\n"; } # Handle deprecated properties - if ( defined( my $value = $ini->val( 'DB', 'database_host' ) ) ) { - $log->warning( "Use of deprecated config property DB.database_host. Use MYSQL.host or POSTGRESQL.host instead." ); + my @warnings; + if ( defined( my $value = $get_and_clear->( 'DB', 'database_host' ) ) ) { + push @warnings, "Use of deprecated config property DB.database_host. Use MYSQL.host or POSTGRESQL.host instead."; $obj->{_MYSQL_host} = $value if $obj->DB_engine eq 'MySQL' && !defined $obj->MYSQL_host; @@ -149,8 +185,8 @@ sub parse { $obj->{_POSTGRESQL_host} = $value if $obj->DB_engine eq 'PostgreSQL' && !defined $obj->POSTGRESQL_host; } - if ( defined( my $value = $ini->val( 'DB', 'user' ) ) ) { - $log->warning( "Use of deprecated config property DB.user. Use MYSQL.user or POSTGRESQL.user instead." ); + if ( defined( my $value = $get_and_clear->( 'DB', 'user' ) ) ) { + push @warnings, "Use of deprecated config property DB.user. Use MYSQL.user or POSTGRESQL.user instead."; $obj->{_MYSQL_user} = $value if $obj->DB_engine eq 'MySQL' && !defined $obj->MYSQL_user; @@ -158,8 +194,8 @@ sub parse { $obj->{_POSTGRESQL_user} = $value if $obj->DB_engine eq 'PostgreSQL' && !defined $obj->POSTGRESQL_user; } - if ( defined( my $value = $ini->val( 'DB', 'password' ) ) ) { - $log->warning( "Use of deprecated config property DB.password. Use MYSQL.password or POSTGRESQL.password instead." ); + if ( defined( my $value = $get_and_clear->( 'DB', 'password' ) ) ) { + push @warnings, "Use of deprecated config property DB.password. Use MYSQL.password or POSTGRESQL.password instead."; $obj->{_MYSQL_password} = $value if $obj->DB_engine eq 'MySQL' && !defined $obj->MYSQL_password; @@ -167,8 +203,8 @@ sub parse { $obj->{_POSTGRESQL_password} = $value if $obj->DB_engine eq 'PostgreSQL' && !defined $obj->POSTGRESQL_password; } - if ( defined( my $value = $ini->val( 'DB', 'database_name' ) ) ) { - $log->warning( "Use of deprecated config property DB.database_name. Use MYSQL.database, POSTGRESQL.database or SQLITE.database_file instead." ); + if ( defined( my $value = $get_and_clear->( 'DB', 'database_name' ) ) ) { + push @warnings, "Use of deprecated config property DB.database_name. Use MYSQL.database, POSTGRESQL.database or SQLITE.database_file instead."; $obj->{_MYSQL_database} = $value if $obj->DB_engine eq 'MySQL' && !defined $obj->MYSQL_database; @@ -179,19 +215,67 @@ sub parse { $obj->{_SQLITE_database_file} = $value if $obj->DB_engine eq 'SQLite' && !defined $obj->SQLITE_database_file; } - if ( defined( my $value = $ini->val( 'ZONEMASTER', 'number_of_professes_for_frontend_testing' ) ) ) { - $log->warning( "Use of deprecated config property ZONEMASTER.number_of_professes_for_frontend_testing. Use ZONEMASTER.number_of_processes_for_frontend_testing instead." ); + if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'number_of_professes_for_frontend_testing' ) ) ) { + push @warnings, "Use of deprecated config property ZONEMASTER.number_of_professes_for_frontend_testing. Use ZONEMASTER.number_of_processes_for_frontend_testing instead."; $obj->{_ZONEMASTER_number_of_processes_for_frontend_testing} = $value if !defined $obj->NumberOfProcessesForFrontendTesting; } - if ( defined( my $value = $ini->val( 'ZONEMASTER', 'number_of_professes_for_batch_testing' ) ) ) { - $log->warning( "Use of deprecated config property ZONEMASTER.number_of_professes_for_batch_testing. Use ZONEMASTER.number_of_processes_for_batch_testing instead." ); + if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'number_of_professes_for_batch_testing' ) ) ) { + push @warnings, "Use of deprecated config property ZONEMASTER.number_of_professes_for_batch_testing. Use ZONEMASTER.number_of_processes_for_batch_testing instead."; $obj->{_ZONEMASTER_number_of_processes_for_batch_testing} = $value if !defined $obj->NumberOfProcessesForBatchTesting; } + # Check unknown property names + my @unrecognized; + for my $section ( $ini->Sections ) { + for my $param ( $ini->Parameters( $section ) ) { + push @unrecognized, "$section.$param"; + } + } + if ( @unrecognized ) { + die "config: unrecognized property(s): " . join( ", ", sort @unrecognized ) . "\n"; + } + + # Check required propertys (part 2/2) + if ( $obj->DB_engine eq 'MySQL' ) { + die "config: missing required property MYSQL.host (required when DB.engine = MySQL and DB.database_host is unset)\n" + if !defined $obj->MYSQL_host; + + die "config: missing required property MYSQL.user (required when DB.engine = MySQL and DB.user is unset)\n" + if !defined $obj->MYSQL_user; + + die "config: missing required property MYSQL.password (required when DB.engine = MySQL and DB.password is unset)\n" + if !defined $obj->MYSQL_password; + + die "config: missing required property MYSQL.database (required when DB.engine = MySQL and DB.database_name is unset)\n" + if !defined $obj->MYSQL_database; + } + elsif ( $obj->DB_engine eq 'PostgreSQL' ) { + die "config: missing required property POSTGRESQL.host (required when DB.engine = PostgreSQL and DB.database_host is unset)\n" + if !defined $obj->POSTGRESQL_host; + + die "config: missing required property POSTGRESQL.user (required when DB.engine = PostgreSQL and DB.user is unset)\n" + if !defined $obj->POSTGRESQL_user; + + die "config: missing required property POSTGRESQL.password (required when DB.engine = PostgreSQL and DB.password is unset)\n" + if !defined $obj->POSTGRESQL_password; + + die "config: missing required property POSTGRESQL.database (required when DB.engine = PostgreSQL and DB.database_name is unset)\n" + if !defined $obj->POSTGRESQL_database; + } + elsif ( $obj->DB_engine eq 'SQLite' ) { + die "config: missing required property SQLITE.database_file (required when DB.engine = SQLite and DB.database_name is unset)\n" + if !defined $obj->SQLITE_database_file; + } + + # Emit deprecation warnings + for my $message ( @warnings ) { + $log->warning( $message ); + } + return $obj; } diff --git a/share/backend_config.ini b/share/backend_config.ini index 7fb816d72..cd5a236f3 100755 --- a/share/backend_config.ini +++ b/share/backend_config.ini @@ -21,10 +21,10 @@ database = zonemaster database_file = /var/lib/zonemaster/db.sqlite [ZONEMASTER] -max_zonemaster_execution_time = 300 -number_of_processes_for_frontend_testing = 20 -number_of_processes_for_batch_testing = 20 -#age_reuse_previous_test=600 +#max_zonemaster_execution_time = 600 +#number_of_processes_for_frontend_testing = 20 +#number_of_processes_for_batch_testing = 20 +#age_reuse_previous_test = 600 # WARNING: The following option is experimental and all edge cases are not fully tested. # Do not use it (keep the default value "0"), or use it with care. diff --git a/t/config.t b/t/config.t index c2819b016..fb0027898 100644 --- a/t/config.t +++ b/t/config.t @@ -143,6 +143,34 @@ subtest 'Everything but NoWarnings' => sub { } qr/Failed to parse config/, 'die: Invalid INI format'; + throws_ok { + my $text = q{ + [DB] + engine = Excel + + [SQLITE] + databse_file = /var/db/zonemaster.sqlite + + [ZNMEOTAESR] + lock_on_queue = 1 + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr{section.*ZNMEOTAESR}, 'die: Invalid section name'; + + throws_ok { + my $text = q{ + [DB] + engine = SQLite + pnlilog_iatnvrel = 0.5 + + [SQLITE] + database_file = /var/db/zonemaster.sqlite + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr{property.*pnlilog_iatnvrel}, 'die: Invalid property name'; + throws_ok { my $text = q{ [DB] @@ -152,6 +180,118 @@ subtest 'Everything but NoWarnings' => sub { } qr/DB\.engine.*Excel/, 'die: Invalid DB.engine value'; + throws_ok { + my $text = q{ + [DB] + engine = MySQL + + [MYSQL] + user = zonemaster_user + password = zonemaster_password + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/MYSQL\.host/, 'die: Missing MYSQL.host value'; + + throws_ok { + my $text = q{ + [DB] + engine = MySQL + + [MYSQL] + host = zonemaster-host + password = zonemaster_password + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/MYSQL\.user/, 'die: Missing MYSQL.user value'; + + throws_ok { + my $text = q{ + [DB] + engine = MySQL + + [MYSQL] + host = zonemaster-host + user = zonemaster_user + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/MYSQL\.password/, 'die: Missing MYSQL.password value'; + + throws_ok { + my $text = q{ + [DB] + engine = MySQL + + [MYSQL] + host = zonemaster-host + user = zonemaster_user + password = zonemaster_password + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/MYSQL\.database/, 'die: Missing MYSQL.database value'; + + throws_ok { + my $text = q{ + [DB] + engine = PostgreSQL + + [POSTGRESQL] + user = zonemaster_user + password = zonemaster_password + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/POSTGRESQL\.host/, 'die: Missing POSTGRESQL.host value'; + + throws_ok { + my $text = q{ + [DB] + engine = PostgreSQL + + [POSTGRESQL] + host = zonemaster-host + password = zonemaster_password + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/POSTGRESQL\.user/, 'die: Missing POSTGRESQL.user value'; + + throws_ok { + my $text = q{ + [DB] + engine = PostgreSQL + + [POSTGRESQL] + host = zonemaster-host + user = zonemaster_user + database = zonemaster_database + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/POSTGRESQL\.password/, 'die: Missing POSTGRESQL.password value'; + + throws_ok { + my $text = q{ + [DB] + engine = PostgreSQL + + [POSTGRESQL] + host = zonemaster-host + user = zonemaster_user + password = zonemaster_password + }; + Zonemaster::Backend::Config->parse( $text ); + } + qr/POSTGRESQL\.database/, 'die: Missing POSTGRESQL.database value'; + throws_ok { my $text = q{ [DB]