X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Fsbin%2Frt-setup-database.in;h=c4bb384317c8bf4376fbaef20645f0b17ace29f4;hp=125708847e565134bc7744199d409a37144e5190;hb=HEAD;hpb=fb4ab1073f0d15d660c6cdc4e07afebf68ef3924 diff --git a/rt/sbin/rt-setup-database.in b/rt/sbin/rt-setup-database.in index 125708847..11e23581b 100644 --- a/rt/sbin/rt-setup-database.in +++ b/rt/sbin/rt-setup-database.in @@ -3,7 +3,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -48,27 +48,20 @@ # END BPS TAGGED BLOCK }}} use strict; use warnings; +use 5.010; use vars qw($Nobody $SystemUser $item); # fix lib paths, some may be relative -BEGIN { +BEGIN { # BEGIN RT CMD BOILERPLATE require File::Spec; + require Cwd; my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); my $bin_path; for my $lib (@libs) { unless ( File::Spec->file_name_is_absolute($lib) ) { - unless ($bin_path) { - if ( File::Spec->file_name_is_absolute(__FILE__) ) { - $bin_path = ( File::Spec->splitpath(__FILE__) )[1]; - } - else { - require FindBin; - no warnings "once"; - $bin_path = $FindBin::Bin; - } - } + $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); } unshift @INC, $lib; @@ -76,34 +69,61 @@ BEGIN { } -#This drags in RT's config.pm -# We do it in a begin block because RT::Handle needs to know the type to do its -# inheritance -BEGIN { - use RT; - RT::LoadConfig(); - RT::InitClasses(); -} - use Term::ReadKey; use Getopt::Long; +use Data::GUID; $| = 1; # unbuffer all output. -my %args; +my %args = ( + package => 'RT', +); GetOptions( \%args, 'action=s', 'force', 'debug', - 'dba=s', 'dba-password=s', 'prompt-for-dba-password', - 'datafile=s', 'datadir=s' + 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s', + 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s', + 'package=s', 'ext-version=s', + 'upgrade-from=s', 'upgrade-to=s', + 'help|h', ); -unless ( $args{'action'} ) { - help(); - exit(-1); +no warnings 'once'; +if ( $args{help} || ! $args{'action'} ) { + require Pod::Usage; + Pod::Usage::pod2usage({ verbose => 2 }); + exit; +} + +require RT; +RT->LoadConfig(); +RT->InitClasses(); + +# Force warnings to be output to STDERR if we're not already logging +# them at a higher level +RT->Config->Set( LogToSTDERR => 'warning') + unless ( RT->Config->Get( 'LogToSTDERR' ) + && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ ); +RT::InitLogging(); + +# get customized root password +my $root_password; +if ( $args{'root-password-file'} ) { + open( my $fh, '<', $args{'root-password-file'} ) + or die "Couldn't open 'args{'root-password-file'}' for reading: $!"; + $root_password = <$fh>; + chomp $root_password; + my $min_length = RT->Config->Get('MinimumPasswordLength'); + if ($min_length) { + die +"password needs to be at least $min_length long, please check file '$args{'root-password-file'}'" + if length $root_password < $min_length; + } + close $fh; } + # check and setup @actions my @actions = grep $_, split /,/, $args{'action'}; if ( @actions > 1 && $args{'datafile'} ) { @@ -111,7 +131,7 @@ if ( @actions > 1 && $args{'datafile'} ) { exit(-1); } foreach ( @actions ) { - unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) { + unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) { print STDERR "$0 called with an invalid --action parameter.\n"; exit(-1); } @@ -124,7 +144,11 @@ foreach ( @actions ) { # convert init to multiple actions my $init = 0; if ( $actions[0] eq 'init' ) { - @actions = qw(create schema acl coredata insert); + if ($args{'skip-create'}) { + @actions = qw(schema coredata insert); + } else { + @actions = qw(create schema acl coredata insert); + } $init = 1; } @@ -137,6 +161,7 @@ foreach my $key(qw(Type Host Name User Password)) { my $db_type = RT->Config->Get('DatabaseType') || ''; my $db_host = RT->Config->Get('DatabaseHost') || ''; +my $db_port = RT->Config->Get('DatabasePort') || ''; my $db_name = RT->Config->Get('DatabaseName') || ''; my $db_user = RT->Config->Get('DatabaseUser') || ''; my $db_pass = RT->Config->Get('DatabasePassword') || ''; @@ -154,27 +179,46 @@ my $dba_pass = exists($args{'dba-password'}) ? $args{'dba-password'} : $ENV{'RT_DBA_PASSWORD'}; -if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) { - $dba_pass = get_dba_password(); - chomp $dba_pass if defined($dba_pass); +if ($args{'skip-create'}) { + $dba_user = $db_user; + $dba_pass = $db_pass; +} else { + if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) { + $dba_pass = get_dba_password(); + chomp $dba_pass if defined($dba_pass); + } } +my $version_word_regex = join '|', RT::Handle->version_words; +my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/; + print "Working with:\n" - ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n" - ."User:\t$db_user\nDBA:\t$dba_user\n"; + ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n" + ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n"; + +my $package = $args{'package'} || 'RT'; +my $ext_version = $args{'ext-version'}; +my $full_id = Data::GUID->new->as_string; + +my $log_actions = 0; +if ($args{'package'} ne 'RT') { + RT->ConnectToDatabase(); + RT->InitSystemObjects(); + $log_actions = 1; +} foreach my $action ( @actions ) { no strict 'refs'; my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args ); error($action, $msg) unless $status; - print $msg ."\n" if $msg; + print $msg .".\n" if $msg; print "Done.\n"; } sub action_create { my %args = @_; my $dbh = get_system_dbh(); - my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' ); + my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' ); return ($status, $msg) unless $status; print "Now creating a $db_type database $db_name for RT.\n"; @@ -188,7 +232,7 @@ sub action_drop { unless ( $args{'force'} ) { print <CheckCompatibility( $dbh, 'pre' ); + my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' ); return ($status, $msg) unless $status; + my $individual_id = Data::GUID->new->as_string(); + my %upgrade_data = ( + action => 'schema', + filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), + stage => 'before', + full_id => $full_id, + individual_id => $individual_id, + ); + $upgrade_data{'ext_version'} = $ext_version if $ext_version; + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + print "Now populating database schema.\n"; - return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} ); + my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} ); + + %upgrade_data = ( + stage => 'after', + individual_id => $individual_id, + return_value => [ @ret ], + ); + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + + return @ret; } sub action_acl { my %args = @_; my $dbh = get_admin_dbh(); - my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' ); + my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' ); return ($status, $msg) unless $status; - print "Now inserting database ACLs\n"; - return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} ); + my $individual_id = Data::GUID->new->as_string(); + my %upgrade_data = ( + action => 'acl', + filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), + stage => 'before', + full_id => $full_id, + individual_id => $individual_id, + ); + $upgrade_data{'ext_version'} = $ext_version if $ext_version; + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + + print "Now inserting database ACLs.\n"; + my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} ); + + %upgrade_data = ( + stage => 'after', + individual_id => $individual_id, + return_value => [ @ret ], + ); + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + + return @ret; +} + +sub action_indexes { + my %args = @_; + RT->ConnectToDatabase; + my $individual_id = Data::GUID->new->as_string(); + my %upgrade_data = ( + action => 'indexes', + filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), + stage => 'before', + full_id => $full_id, + individual_id => $individual_id, + ); + $upgrade_data{'ext_version'} = $ext_version if $ext_version; + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + + my $dbh = get_admin_dbh(); + $RT::Handle = RT::Handle->new; + $RT::Handle->dbh( $dbh ); + RT::InitLogging(); + + print "Now inserting database indexes.\n"; + my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} ); + + $RT::Handle = RT::Handle->new; + $RT::Handle->dbh( undef ); + RT->ConnectToDatabase; + %upgrade_data = ( + stage => 'after', + individual_id => $individual_id, + return_value => [ @ret ], + ); + RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; + + return @ret; } sub action_coredata { my %args = @_; - $RT::Handle = new RT::Handle; + $RT::Handle = RT::Handle->new; $RT::Handle->dbh( undef ); RT::ConnectToDatabase(); - RT::InitLogging(); - my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' ); + my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' ); return ($status, $msg) unless $status; - print "Now inserting RT core system objects\n"; + print "Now inserting RT core system objects.\n"; return $RT::Handle->InsertInitialData; } sub action_insert { + state $RAN_INIT; my %args = @_; - $RT::Handle = new RT::Handle; - RT::Init(); - my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' ); + unless ($RAN_INIT) { + $RT::Handle = RT::Handle->new; + RT::Init(); + $RAN_INIT++; + } + $log_actions = 1; + + my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' ); return ($status, $msg) unless $status; - print "Now inserting data\n"; + print "Now inserting data.\n"; my $file = $args{'datafile'}; $file = $RT::EtcPath . "/initialdata" if $init && !$file; $file ||= $args{'datadir'}."/content"; - return $RT::Handle->InsertData( $file ); + + my $individual_id = Data::GUID->new->as_string(); + my %upgrade_data = ( + action => 'insert', + filename => Cwd::abs_path($file), + stage => 'before', + full_id => $full_id, + individual_id => $individual_id + ); + $upgrade_data{'ext_version'} = $ext_version if $ext_version; + + open my $handle, '<', $file or warn "Unable to open $file: $!"; + $upgrade_data{content} = do {local $/; <$handle>} if $handle; + + RT->System->AddUpgradeHistory($package => \%upgrade_data); + + my @ret; + + my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) }; + + for my $file (@{$args{backcompat} || []}) { + my $lines = do {local $/; local @ARGV = ($file); <>}; + my $sub = eval "sub {\n# line 1 $file\n$lines\n}"; + unless ($sub) { + warn "Failed to load backcompat $file: $@"; + next; + } + my $current = $upgrade; + $upgrade = sub { $sub->($current) }; + } + + $upgrade->(); + + # XXX Reconnecting to insert the history entry + # until we can sort out removing + # the disconnect at the end of InsertData. + RT->ConnectToDatabase(); + + %upgrade_data = ( + stage => 'after', + individual_id => $individual_id, + return_value => [ @ret ], + ); + + RT->System->AddUpgradeHistory($package => \%upgrade_data); + + my $db_type = RT->Config->Get('DatabaseType'); + $RT::Handle->Disconnect() unless $db_type eq 'SQLite'; + + return @ret; } sub action_upgrade { @@ -257,12 +430,12 @@ sub action_upgrade { if ( defined $upgrading_from ) { print "Doesn't match #.#.#: "; } else { - print "Enter RT version you're upgrading from: "; + print "Enter $args{package} version you're upgrading from: "; } - $upgrading_from = scalar ; + $upgrading_from = $args{'upgrade-from'} || scalar ; chomp $upgrading_from; $upgrading_from =~ s/\s+//g; - } while $upgrading_from !~ /^\d+\.\d+\.\d+$/; + } while $upgrading_from !~ /$version_dir/; my $upgrading_to = $RT::VERSION; return (0, "The current version $upgrading_to is lower than $upgrading_from") @@ -271,11 +444,16 @@ sub action_upgrade { return (1, "The version $upgrading_to you're upgrading to is up to date") if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0; - my @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to); - - return (1, "No DB changes between $upgrading_from and $upgrading_to") + my @versions = get_versions_from_to($base_dir, $upgrading_from, undef); + return (1, "No DB changes since $upgrading_from") unless @versions; + if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) { + print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n"; + print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n"; + $upgrading_to = $versions[-1]; + } + print "\nGoing to apply following upgrades:\n"; print map "* $_\n", @versions; @@ -285,14 +463,14 @@ sub action_upgrade { if ( defined $custom_upgrading_to ) { print "Doesn't match #.#.#: "; } else { - print "\nEnter RT version if you want to stop upgrade at some point,\n"; + print "\nEnter $args{package} version if you want to stop upgrade at some point,\n"; print " or leave it blank if you want apply above upgrades: "; } - $custom_upgrading_to = scalar ; + $custom_upgrading_to = $args{'upgrade-to'} || scalar ; chomp $custom_upgrading_to; $custom_upgrading_to =~ s/\s+//g; last unless $custom_upgrading_to; - } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+$/; + } while $custom_upgrading_to !~ /$version_dir/; if ( $custom_upgrading_to ) { return ( @@ -319,22 +497,97 @@ sub action_upgrade { } } - print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n"; - _yesno() or exit(-2) unless $args{'force'}; + unless ( $args{'force'} ) { + print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n"; + _yesno() or exit(-2); + } + + RT->ConnectToDatabase(); + RT->InitSystemObjects(); + $log_actions = 1; + + RT->System->AddUpgradeHistory($package => { + type => 'full upgrade', + action => 'upgrade', + stage => 'before', + from => $upgrading_from, + to => $upgrading_to, + versions => [@versions], + full_id => $full_id, + individual_id => $full_id + }); + + # Ensure that the Attributes column is big enough to hold the + # upgrade steps we're going to add; this step exists in 4.0.6 for + # mysql, but that may be too late. Run it as soon as possible. + if (RT->Config->Get('DatabaseType') eq 'mysql' + and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) { + my $dbh = get_admin_dbh(); + # Before the binary switch in 3.7.87, we want to alter text -> + # longtext, not blob -> longblob + if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) { + $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT") + } else { + $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB") + } + } + + my $previous = $upgrading_from; + my ( $ret, $msg ); + foreach my $n ( 0..$#versions ) { + my $v = $versions[$n]; + my $individual_id = Data::GUID->new->as_string(); - foreach my $v ( @versions ) { + my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions; print "Processing $v\n"; - my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef); + + RT->System->AddUpgradeHistory($package => { + action => 'upgrade', + type => 'individual upgrade', + stage => 'before', + from => $previous, + to => $v, + full_id => $full_id, + individual_id => $individual_id, + }); + + my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back); + if ( -e "$base_dir/$v/schema.$db_type" ) { - action_schema( %tmp ); + ( $ret, $msg ) = action_schema( %tmp ); + return ( $ret, $msg ) unless $ret; } if ( -e "$base_dir/$v/acl.$db_type" ) { - action_acl( %tmp ); + ( $ret, $msg ) = action_acl( %tmp ); + return ( $ret, $msg ) unless $ret; + } + if ( -e "$base_dir/$v/indexes" ) { + ( $ret, $msg ) = action_indexes( %tmp ); + return ( $ret, $msg ) unless $ret; } if ( -e "$base_dir/$v/content" ) { - action_insert( %tmp ); + ( $ret, $msg ) = action_insert( %tmp ); + return ( $ret, $msg ) unless $ret; } + + # XXX: Another connect since the insert called + # previous to this step will disconnect. + + RT->ConnectToDatabase(); + + RT->System->AddUpgradeHistory($package => { + stage => 'after', + individual_id => $individual_id, + }); + + $previous = $v; } + + RT->System->AddUpgradeHistory($package => { + stage => 'after', + individual_id => $full_id, + }); + return 1; } @@ -342,11 +595,14 @@ sub get_versions_from_to { my ($base_dir, $from, $to) = @_; opendir( my $dh, $base_dir ) or die "couldn't open dir: $!"; - my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh; + my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh; closedir $dh; + die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n" + unless @versions; + return - grep RT::Handle::cmp_version($_, $to) <= 0, + grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1, grep RT::Handle::cmp_version($_, $from) > 0, sort RT::Handle::cmp_version @versions; } @@ -361,7 +617,7 @@ sub error { sub get_dba_password { print "In order to create or update your RT database," . " this script needs to connect to your " - . " $db_type instance on $db_host as $dba_user\n"; + . " $db_type instance on $db_host (port '$db_port') as $dba_user\n"; print "Please specify that user's database password below. If the user has no database\n"; print "password, just press return.\n\n"; print "Password: "; @@ -372,13 +628,10 @@ sub get_dba_password { return ($password); } -=head2 get_system_dbh +# get_system_dbh +# Returns L database handle connected to B with DBA credentials. +# See also L. -Returns L database handle connected to B with DBA credentials. - -See also L. - -=cut sub get_system_dbh { return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass ); @@ -388,13 +641,11 @@ sub get_admin_dbh { return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass ); } -=head2 get_rt_dbh [USER, PASSWORD] - -Returns L database handle connected to RT database, -you may specify credentials(USER and PASSWORD) to connect -with. By default connects with credentials from RT config. +# get_rt_dbh [USER, PASSWORD] -=cut +# Returns L database handle connected to RT database, +# you may specify credentials(USER and PASSWORD) to connect +# with. By default connects with credentials from RT config. sub get_rt_dbh { return _get_dbh( RT::Handle->DSN, $db_user, $db_pass ); @@ -423,54 +674,129 @@ sub _yesno { $x =~ /^y/i; } -sub help { +1; - print <. + +=item schema + +Initialize only the database schema + +To use a local or supplementary datafile, specify it using the '--datadir' +option below. + +=item acl + +Initialize only the database ACLs + +To use a local or supplementary datafile, specify it using the '--datadir' +option below. + +=item coredata + +Insert data into RT's database. This data is required for normal functioning of +any RT instance. + +=item insert + +Insert data into RT's database. By default, will use RT's installation data. +To use a local or supplementary datafile, specify it using the '--datafile' +option below. + +=back + +=item datafile + +file path of the data you want to action on + +e.g. C<--datafile /path/to/datafile> + +=item datadir + +Used to specify a path to find the local database schema and acls to be +installed. + +e.g. C<--datadir /path/to/> + +=item dba + +dba's username + +=item dba-password + +dba's password + +=item prompt-for-dba-password + +Ask for the database administrator's password interactively + +=item skip-create + +for 'init': skip creating the database and the user account, so we don't need +administrator privileges + +=item root-password-file + +for 'init' and 'insert': rather than using the default administrative password +for RT's "root" user, use the password in this file. + +=item package + +the name of the entity performing a create or upgrade. Used for logging changes +in the DB. Defaults to RT, otherwise it should be the fully qualified package name +of the extension or plugin making changes to the DB. + +=item ext-version + +current version of extension making a change. Not needed for RT since RT has a +more elaborate system to track upgrades across multiple versions. + +=item upgrade-from + +for 'upgrade': specifies the version to upgrade from, and do not prompt +for it if it appears to be a valid version. + +=item upgrade-to + +for 'upgrade': specifies the version to upgrade to, and do not prompt +for it if it appears to be a valid version. + +=back + +=cut