Skip to content

Commit

Permalink
Merge pull request #753 from mattias-p/validate-structure
Browse files Browse the repository at this point in the history
Validate structure
  • Loading branch information
mattias-p committed Apr 30, 2021
2 parents 54be27a + b16962a commit c88148a
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 37 deletions.
150 changes: 117 additions & 33 deletions lib/Zonemaster/Backend/Config.pm
Expand Up @@ -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

Expand All @@ -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);
};
Expand All @@ -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";

Expand All @@ -132,43 +162,49 @@ 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;

$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;

$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;

$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;
Expand All @@ -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;
}

Expand Down
8 changes: 4 additions & 4 deletions share/backend_config.ini
Expand Up @@ -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.
Expand Down
140 changes: 140 additions & 0 deletions t/config.t
Expand Up @@ -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]
Expand All @@ -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]
Expand Down

0 comments on commit c88148a

Please sign in to comment.