2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
7 # <sales@bestpractical.com>
9 # (Except where explicitly superseded by other copyright notices)
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 # General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
31 # CONTRIBUTION SUBMISSION POLICY:
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
48 # END BPS TAGGED BLOCK }}}
52 use vars qw($Nobody $SystemUser $item);
54 # fix lib paths, some may be relative
57 my @libs = ("lib", "local/lib");
61 unless ( File::Spec->file_name_is_absolute($lib) ) {
63 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
64 $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
69 $bin_path = $FindBin::Bin;
72 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
82 $| = 1; # unbuffer all output.
92 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
93 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
98 if ( $args{help} || ! $args{'action'} ) {
100 Pod::Usage::pod2usage({ verbose => 2 });
108 # Force warnings to be output to STDERR if we're not already logging
109 # them at a higher level
110 RT->Config->Set( LogToScreen => 'warning')
111 unless ( RT->Config->Get( 'LogToScreen' )
112 && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ );
114 # get customized root password
116 if ( $args{'root-password-file'} ) {
117 open( my $fh, '<', $args{'root-password-file'} )
118 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
119 $root_password = <$fh>;
120 chomp $root_password;
121 my $min_length = RT->Config->Get('MinimumPasswordLength');
124 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
125 if length $root_password < $min_length;
131 # check and setup @actions
132 my @actions = grep $_, split /,/, $args{'action'};
133 if ( @actions > 1 && $args{'datafile'} ) {
134 print STDERR "You can not use --datafile option with multiple actions.\n";
137 foreach ( @actions ) {
138 unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
139 print STDERR "$0 called with an invalid --action parameter.\n";
142 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
143 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
148 # convert init to multiple actions
150 if ( $actions[0] eq 'init' ) {
151 if ($args{'skip-create'}) {
152 @actions = qw(schema coredata insert);
154 @actions = qw(create schema acl coredata insert);
159 # set options from environment
160 foreach my $key(qw(Type Host Name User Password)) {
161 next unless exists $ENV{ 'RT_DB_'. uc $key };
162 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
163 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
166 my $db_type = RT->Config->Get('DatabaseType') || '';
167 my $db_host = RT->Config->Get('DatabaseHost') || '';
168 my $db_port = RT->Config->Get('DatabasePort') || '';
169 my $db_name = RT->Config->Get('DatabaseName') || '';
170 my $db_user = RT->Config->Get('DatabaseUser') || '';
171 my $db_pass = RT->Config->Get('DatabasePassword') || '';
173 # load it here to get error immidiatly if DB type is not supported
176 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
177 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
178 RT->Config->Set( DatabaseName => $db_name );
181 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
182 my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
184 if ($args{'skip-create'}) {
185 $dba_user = $db_user;
186 $dba_pass = $db_pass;
188 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
189 $dba_pass = get_dba_password();
190 chomp $dba_pass if defined($dba_pass);
194 my $version_word_regex = join '|', RT::Handle->version_words;
195 my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
197 print "Working with:\n"
198 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
199 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
201 foreach my $action ( @actions ) {
203 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
204 error($action, $msg) unless $status;
205 print $msg .".\n" if $msg;
211 my $dbh = get_system_dbh();
212 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
213 return ($status, $msg) unless $status;
215 print "Now creating a $db_type database $db_name for RT.\n";
216 return RT::Handle->CreateDatabase( $dbh );
222 print "Dropping $db_type database $db_name.\n";
223 unless ( $args{'force'} ) {
226 About to drop $db_type database $db_name on $db_host (port '$db_port').
227 WARNING: This will erase all data in $db_name.
230 exit(-2) unless _yesno();
233 my $dbh = get_system_dbh();
234 return RT::Handle->DropDatabase( $dbh );
239 my $dbh = get_admin_dbh();
240 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
241 return ($status, $msg) unless $status;
243 print "Now populating database schema.\n";
244 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
249 my $dbh = get_admin_dbh();
250 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
251 return ($status, $msg) unless $status;
253 print "Now inserting database ACLs.\n";
254 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
257 sub action_coredata {
259 $RT::Handle = RT::Handle->new;
260 $RT::Handle->dbh( undef );
261 RT::ConnectToDatabase();
263 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
264 return ($status, $msg) unless $status;
266 print "Now inserting RT core system objects.\n";
267 return $RT::Handle->InsertInitialData;
272 $RT::Handle = RT::Handle->new;
274 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
275 return ($status, $msg) unless $status;
277 print "Now inserting data.\n";
278 my $file = $args{'datafile'};
279 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
280 $file ||= $args{'datadir'}."/content";
282 # Slurp in backcompat
284 my @back = @{$args{backcompat} || []};
286 my @lines = do {local @ARGV = @back; <>};
290 my ($class, @fields) = split;
291 $class->_BuildTableAttributes;
292 $RT::Logger->debug("Temporarily removing @fields from $class");
293 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
298 my @ret = $RT::Handle->InsertData( $file, $root_password );
300 # Put back the fields we chopped off
301 for my $class (keys %removed) {
302 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
303 for keys %{$removed{$class}};
310 my $base_dir = $args{'datadir'} || "./etc/upgrade";
311 return (0, "Couldn't read dir '$base_dir' with upgrade data")
312 unless -d $base_dir || -r _;
314 my $upgrading_from = undef;
316 if ( defined $upgrading_from ) {
317 print "Doesn't match #.#.#: ";
319 print "Enter $args{package} version you're upgrading from: ";
321 $upgrading_from = scalar <STDIN>;
322 chomp $upgrading_from;
323 $upgrading_from =~ s/\s+//g;
324 } while $upgrading_from !~ /$version_dir/;
326 my $upgrading_to = $RT::VERSION;
327 return (0, "The current version $upgrading_to is lower than $upgrading_from")
328 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
330 return (1, "The version $upgrading_to you're upgrading to is up to date")
331 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
333 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
334 return (1, "No DB changes since $upgrading_from")
337 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
338 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
339 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
340 $upgrading_to = $versions[-1];
343 print "\nGoing to apply following upgrades:\n";
344 print map "* $_\n", @versions;
347 my $custom_upgrading_to = undef;
349 if ( defined $custom_upgrading_to ) {
350 print "Doesn't match #.#.#: ";
352 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
353 print " or leave it blank if you want apply above upgrades: ";
355 $custom_upgrading_to = scalar <STDIN>;
356 chomp $custom_upgrading_to;
357 $custom_upgrading_to =~ s/\s+//g;
358 last unless $custom_upgrading_to;
359 } while $custom_upgrading_to !~ /$version_dir/;
361 if ( $custom_upgrading_to ) {
363 0, "The version you entered ($custom_upgrading_to) is lower than\n"
364 ."version you're upgrading from ($upgrading_from)"
365 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
367 return (1, "The version you're upgrading to is up to date")
368 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
370 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
371 print "Version you entered is greater than installed ($RT::VERSION).\n";
372 _yesno() or exit(-2);
374 # ok, checked everything no let's refresh list
375 $upgrading_to = $custom_upgrading_to;
376 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
378 return (1, "No DB changes between $upgrading_from and $upgrading_to")
381 print "\nGoing to apply following upgrades:\n";
382 print map "* $_\n", @versions;
386 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
387 _yesno() or exit(-2) unless $args{'force'};
390 foreach my $n ( 0..$#versions ) {
391 my $v = $versions[$n];
392 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
393 print "Processing $v\n";
394 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
395 if ( -e "$base_dir/$v/schema.$db_type" ) {
396 ( $ret, $msg ) = action_schema( %tmp );
397 return ( $ret, $msg ) unless $ret;
399 if ( -e "$base_dir/$v/acl.$db_type" ) {
400 ( $ret, $msg ) = action_acl( %tmp );
401 return ( $ret, $msg ) unless $ret;
403 if ( -e "$base_dir/$v/content" ) {
404 ( $ret, $msg ) = action_insert( %tmp );
405 return ( $ret, $msg ) unless $ret;
411 sub get_versions_from_to {
412 my ($base_dir, $from, $to) = @_;
414 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
415 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
418 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
422 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
423 grep RT::Handle::cmp_version($_, $from) > 0,
424 sort RT::Handle::cmp_version @versions;
428 my ($action, $msg) = @_;
429 print STDERR "Couldn't finish '$action' step.\n\n";
430 print STDERR "ERROR: $msg\n\n";
434 sub get_dba_password {
435 print "In order to create or update your RT database,"
436 . " this script needs to connect to your "
437 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
438 print "Please specify that user's database password below. If the user has no database\n";
439 print "password, just press return.\n\n";
442 my $password = ReadLine(0);
449 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
450 # See also L<RT::Handle/SystemDSN>.
454 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
458 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
461 # get_rt_dbh [USER, PASSWORD]
463 # Returns L<DBI> database handle connected to RT database,
464 # you may specify credentials(USER and PASSWORD) to connect
465 # with. By default connects with credentials from RT config.
468 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
472 my ($dsn, $user, $pass) = @_;
473 my $dbh = DBI->connect(
475 { RaiseError => 0, PrintError => 0 },
478 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
479 if ( $args{'debug'} ) {
480 require Carp; Carp::confess( $msg );
482 print STDERR $msg; exit -1;
489 print "Proceed [y/N]:";
490 my $x = scalar(<STDIN>);
500 rt-setup-database - Set up RT's database
504 rt-setup-database --action ...
512 Several actions can be combined using comma separated list.
518 Initialize the database. This is combination of multiple actions listed below.
519 Create DB, schema, setup acl, insert core data and initial data.
523 Apply all needed schema/acl/content updates (will ask for version to upgrade
532 Drop the database. This will B<ERASE ALL YOUR DATA>.
536 Initialize only the database schema
538 To use a local or supplementary datafile, specify it using the '--datadir'
543 Initialize only the database ACLs
545 To use a local or supplementary datafile, specify it using the '--datadir'
550 Insert data into RT's database. This data is required for normal functioning of
555 Insert data into RT's database. By default, will use RT's installation data.
556 To use a local or supplementary datafile, specify it using the '--datafile'
563 file path of the data you want to action on
565 e.g. C<--datafile /path/to/datafile>
569 Used to specify a path to find the local database schema and acls to be
572 e.g. C<--datadir /path/to/>
582 =item prompt-for-dba-password
584 Ask for the database administrator's password interactively
588 for 'init': skip creating the database and the user account, so we don't need
589 administrator privileges
591 =item root-password-file
593 for 'init' and 'insert': rather than using the default administrative password
594 for RT's "root" user, use the password in this file.