#!/usr/bin/env perl # # $Id: $ # # Copyright (c) 2012 OpenDNSSEC AB (svb). All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; use DBI; use Getopt::Long (); use Pod::Usage (); use File::Basename (); # Try to require Term::ReadKey, ignore if it does not exist eval { require Term::ReadKey; }; my ($sth, $row); my $schema_path; my $from; my $from_username; my $from_password; my $to; my $to_username; my $to_password; my $help; Pod::Usage::pod2usage(1) unless (@ARGV); Getopt::Long::GetOptions( 'help|?' => \$help, 'schema-path:s' => \$schema_path, 'from=s' => \$from, 'from-username:s' => \$from_username, 'from-password:s' => \$from_password, 'to=s' => \$to, 'to-username:s' => \$to_username, 'to-password:s' => \$to_password ) or Pod::Usage::pod2usage(2); Pod::Usage::pod2usage(-verbose => 99, -exitval => 1) if $help; # # Check schema path and existence of database creation scripts # unless (defined $schema_path) { $schema_path = File::Basename::dirname($0); } unless (-r $schema_path.'/database_create.sqlite3' and -r $schema_path.'/database_create.mysql') { print STDERR $0, ': Can not find database creation schemas, please set or correct --schema-path.', "\n"; exit(-1); } # # Check --from, let DBI parse it and check that the correct data sources are used # unless (defined $from) { print STDERR $0, ': Missing required option --from.', "\n"; exit(-1); } my (undef, $from_data_source) = DBI->parse_dsn($from); unless (defined $from_data_source and ($from_data_source eq 'mysql' or $from_data_source eq 'SQLite')) { print STDERR $0, ': Invalid data source used in --from DSN, only mysql or SQLite allowed (case sensitive).', "\n"; exit(-1); } # # Check --to, let DBI parse it and check that the correct data sources are used # unless (defined $to) { print STDERR $0, ': Missing required option --to.', "\n"; exit(-1); } my (undef, $to_data_source) = DBI->parse_dsn($to); unless (defined $to_data_source and ($to_data_source eq 'mysql' or $to_data_source eq 'SQLite')) { print STDERR $0, ': Invalid data source used in --to DSN, only mysql or SQLite allowed (case sensitive).', "\n"; exit(-1); } # # Prompt for --from-password if from DSN is MySQL and password not given # if (!defined $from_password and $from_data_source eq 'mysql') { print 'Enter exporting database password (--from-password): '; # Try Term::ReadKey eval { ReadMode('noecho'); $from_password = ReadLine(0); ReadMode('echo'); }; # Fallback if Term::ReadKey does not exist if ($@) { $from_password = ; } unless ($from_password) { print STDERR $0, ': No password given for exporting database.', "\n"; } $from_password =~ s/[\r\n]+$//o; } # # Connect to exporting database and get the schema version # my $from_dbh; my $from_version; unless ($from_dbh = DBI->connect($from, $from_username, $from_password, { AutoCommit => 1 })) { print STDERR $0, ': Unable to connect to exporting database [', $from, ']: ', $DBI::errstr, "\n"; exit(-1); } print 'Connected to exporting database ', $from, "\n"; $sth = prepare_or_die($from_dbh, 'SELECT version FROM dbadmin'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($from_version = $row->{version})) { $sth->finish; print STDERR $0, ': Unable to get schema version from exporting database', "\n"; exit(-1); } $sth->finish; # # Check for supported database schema version # my $from_version_valid = 0; if ($from_version == 3) { $from_version_valid = 1; } unless ($from_version_valid) { print STDERR $0, ': Exporting database schema version is not supported for conversion.', "\n"; exit(-1); } # # Validate existing data depending on importing data source # if ($to_data_source eq 'mysql') { if ($from_version == 3) { my $valid = 1; print 'Validating existing data', "\n"; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM securitymodules WHERE id > 127'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table securitymodules', "\n"; exit(-1); } if ($row->{count}) { print 'Table securitymodules field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM categories WHERE id > 127'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table categories', "\n"; exit(-1); } if ($row->{count}) { print 'Table categories field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM parameters WHERE id > 8388607'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table parameters', "\n"; exit(-1); } if ($row->{count}) { print 'Table parameters field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM serialmodes WHERE id > 127'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table serialmodes', "\n"; exit(-1); } if ($row->{count}) { print 'Table serialmodes field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM policies WHERE id > 8388607'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table policies', "\n"; exit(-1); } if ($row->{count}) { print 'Table policies field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM zones WHERE id > 8388607'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table zones', "\n"; exit(-1); } if ($row->{count}) { print 'Table zones field id contains too large values', "\n"; $valid = 0; } $sth->finish; $sth = prepare_or_die($from_dbh, 'SELECT COUNT(*) AS "count" FROM parameters_policies WHERE id > 8388607'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($row->{count})) { $sth->finish; print STDERR $0, ': Unable to validate table parameters_policies', "\n"; exit(-1); } if ($row->{count}) { print 'Table parameters_policies field id contains too large values', "\n"; $valid = 0; } $sth->finish; unless ($valid) { print STDERR $0, ': Unable to get schema version from exporting database', "\n"; exit(-1); } } } # # Ask the user if he really wants to create the importing database and delete existing data # print 'Create the importing database, this will delete existing data? [NO/yes] '; my $answer = ; chomp($answer); unless ($answer =~ /^yes$/io) { exit(0); } if ($to_data_source eq 'mysql') { $to .= ';mysql_multi_statements=1'; } # # Prompt for --to-password if from DSN is MySQL and password not given # if (!defined $to_password and $to_data_source eq 'mysql') { print 'Enter importing database password (--to-password): '; # Try Term::ReadKey eval { ReadMode('noecho'); $to_password = ReadLine(0); ReadMode('echo'); }; # Fallback if Term::ReadKey does not exist if ($@) { $to_password = ; } unless ($to_password) { print STDERR $0, ': No password given for importing database.', "\n"; } $to_password =~ s/[\r\n]+$//o; } # # Connect to importing database # my $to_dbh; my $to_version; unless ($to_dbh = DBI->connect($to, $to_username, $to_password, { AutoCommit => 1 })) { print STDERR $0, ': Unable to connect to importing database [', $to, ']: ', $DBI::errstr, "\n"; exit(-1); } print 'Connected to importing database ', $to, "\n"; # # Create the importing database tables # if ($to_data_source eq 'mysql') { unless (open(FILE, $schema_path.'/database_create.mysql')) { print STDERR $0, ': ', "\n"; exit(-1); } my $sql = ''; while ((my $line = )) { if ($line =~ /^\s*--/o) { next; } $line =~ s/\r//go; $sql .= $line; if ($sql =~ /\;$/o) { unless ($to_dbh->do($sql)) { print STDERR $0, ': Unable to create importing database, statement "', $sql, '" failed: ', $to_dbh->errstr, "\n"; exit(-1); } $sql = ''; } } } elsif ($to_data_source eq 'SQLite') { unless (open(FILE, $schema_path.'/database_create.sqlite3')) { print STDERR $0, ': ', "\n"; exit(-1); } my $sql = ''; while ((my $line = )) { if ($line =~ /^\s*--/o) { next; } $line =~ s/\r//go; $sql .= $line; if ($sql =~ /\;$/o) { unless ($to_dbh->do($sql)) { print STDERR $0, ': Unable to create importing database, statement "', $sql, '" failed: ', $to_dbh->errstr, "\n"; exit(-1); } $sql = ''; } } } else { print STDERR $0, ': Invalid data source, internal bug? please report this', "\n"; exit(-1); } # # Get the schema version of the importing database # $sth = prepare_or_die($to_dbh, 'SELECT version FROM dbadmin'); unless ($sth->execute and defined ($row = $sth->fetchrow_hashref) and defined ($to_version = $row->{version})) { $sth->finish; print STDERR $0, ': Unable to get schema version from importing database', "\n"; exit(-1); } $sth->finish; # # Validate that we are using the same exporting and importing schema version # unless ($from_version == $to_version) { print STDERR $0, ': Database schema version missmatch [from: ', $from_version, ' to: ', $to_version, '], can not convert databases of different schema versions, please upgrade existing installation first.', "\n"; exit(-1); } # # Convert the database # if ($from_version == 3) { # # Schema version 3 does not need any data modifications so just dump it out and in # my @tables = ( { securitymodules => { delete => 'DELETE FROM securitymodules', select => 'SELECT * FROM securitymodules', insert => 'INSERT INTO securitymodules VALUES ( ?, ?, ?, ? )' }}, { categories => { delete => 'DELETE FROM categories', select => 'SELECT * FROM categories', insert => 'INSERT INTO categories VALUES ( ?, ? )' }}, { parameters => { delete => 'DELETE FROM parameters', select => 'SELECT * FROM parameters', insert => 'INSERT INTO parameters VALUES ( ?, ?, ?, ? )' }}, { serialmodes => { delete => 'DELETE FROM serialmodes', select => 'SELECT * FROM serialmodes', insert => 'INSERT INTO serialmodes VALUES ( ?, ?, ? )' }}, { policies => { delete => 'DELETE FROM policies', select => 'SELECT * FROM policies', insert => 'INSERT INTO policies VALUES ( ?, ?, ?, ?, ?, ? )' }}, { zones => { delete => 'DELETE FROM zones', select => 'SELECT * FROM zones', insert => 'INSERT INTO zones VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )' }}, { keypairs => { delete => 'DELETE FROM keypairs', select => 'SELECT * FROM keypairs', insert => 'INSERT INTO keypairs VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )' }}, { dnsseckeys => { delete => 'DELETE FROM dnsseckeys', select => 'SELECT * FROM dnsseckeys', insert => 'INSERT INTO dnsseckeys VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )' }}, { parameters_policies => { delete => 'DELETE FROM parameters_policies', select => 'SELECT * FROM parameters_policies', insert => 'INSERT INTO parameters_policies VALUES ( ?, ?, ?, ? )' }} ); print 'Initializing conversion', "\n"; foreach my $table (@tables) { my ($name) = keys %$table; $table->{$name}->{del_sth} = prepare_or_die($to_dbh, $table->{$name}->{delete}); $table->{$name}->{sel_sth} = prepare_or_die($from_dbh, $table->{$name}->{select}); $table->{$name}->{ins_sth} = prepare_or_die($to_dbh, $table->{$name}->{insert}); } print 'Deleting existing data in importing database', "\n"; foreach my $table (reverse(@tables)) { my ($name) = keys %$table; unless ($table->{$name}->{del_sth}->execute) { $table->{$name}->{del_sth}->finish; print STDERR $0, ': Unable to delete existing data in importing database table ', $name, ': ', $table->{$name}->{del_sth}->errstr, "\n"; exit(-1); } $table->{$name}->{del_sth}->finish; } print 'Converting database', "\n"; foreach my $table (@tables) { my ($name) = keys %$table; print "\t", $name, "\n"; unless ($table->{$name}->{sel_sth}->execute) { $table->{$name}->{sel_sth}->finish; print STDERR $0, ': Unable to select from exporting database table ', $name, ': ', $table->{$name}->{sel_sth}->errstr, "\n"; exit(-1); } while (defined (my $row = $table->{$name}->{sel_sth}->fetchrow_arrayref)) { unless ($table->{$name}->{ins_sth}->execute(@$row)) { $table->{$name}->{ins_sth}->finish; print STDERR $0, ': Unable to insert into importing database table ', $name, ': ', $table->{$name}->{ins_sth}->errstr, "\n"; exit(-1); } $table->{$name}->{ins_sth}->finish; } $table->{$name}->{sel_sth}->finish; } print 'Optimizing database', "\n"; if ($to_data_source eq 'mysql') { foreach my $table (@tables) { my ($name) = keys %$table; unless ($to_dbh->do('OPTIMIZE TABLE '.$name)) { print STDERR $0, ': Unable to OPTIMIZE TABLE ', $name, ': ', $to_dbh->errstr, "\n"; exit(-1); } } } elsif ($to_data_source eq 'SQLite') { unless ($to_dbh->do('VACUUM')) { print STDERR $0, ': Unable to VACUUM database: ', $to_dbh->errstr, "\n"; exit(-1); } unless ($to_dbh->do('ANALYZE')) { print STDERR $0, ': Unable to ANALYZE database: ', $to_dbh->errstr, "\n"; exit(-1); } } else { print STDERR $0, ': Invalid data source, internal bug? please report this', "\n"; exit(-1); } } print 'Done', "\n"; exit 0; # # Close connections on exit() if they exist # END { if (defined $from_dbh) { $from_dbh->disconnect; } if (defined $to_dbh) { $to_dbh->disconnect; } } # # Prepare a statement or die trying # sub prepare_or_die { my ($dbh, $statement) = @_; my $sth; unless (defined ($sth = $dbh->prepare($statement))) { print STDERR $0, ': Unable to prepare statement "', $statement, '": ', $dbh->errstr, "\n"; exit(-1); } $sth; } __END__ =head1 NAME convert_database.pl - OpenDNSSEC database conversion tool =head1 SYNOPSIS convert_database.pl [options] =head1 OPTIONS =over 8 =item B<--schema-path > Specify the path to the directory containing the database schemas. (default to same path as convert_database.pl) =item B<--from > Specify DBI DSN database connection string to use as the exporting database, see man/perldoc DBD::mysql or DBD::SQLite for more information. (Required) =item B<--from-username > Username for the exporting database (default current logged in user). =item B<--from-password > Password to the exporting database (default prompted if MySQL). =item B<--to > Specify DBI DSN database connection string to use as the importing database, see man/perldoc DBD::mysql or DBD::SQLite for more information. (Required) =item B<--to-username > Username for the importing database (default current logged in user). =item B<--to-password > Password to the importing database (default prompted if MySQL). =item B<--help> Print a brief help message and exits. =back =head1 DESCRIPTION This program converts OpenDNSSEC Enforcer database from one backend to another. =head1 EXAMPLES This example converts from a SQLite to a MySQL database using default installation paths: convert_database.pl --from dbi:SQLite:dbname=/var/opendnssec/kasp.db --to dbi:mysql:database=kasp;host=localhost --to-username kasp --to-password kasp =cut