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.
91 'dba=s', 'dba-password=s', 'prompt-for-dba-password',
92 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
97 if ( $args{help} || ! $args{'action'} ) {
99 Pod::Usage::pod2usage({ verbose => 2 });
107 # Force warnings to be output to STDERR if we're not already logging
108 # them at a higher level
109 RT->Config->Set( LogToScreen => 'warning')
110 unless ( RT->Config->Get( 'LogToScreen' )
111 && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ );
113 # get customized root password
115 if ( $args{'root-password-file'} ) {
116 open( my $fh, '<', $args{'root-password-file'} )
117 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
118 $root_password = <$fh>;
119 chomp $root_password;
120 my $min_length = RT->Config->Get('MinimumPasswordLength');
123 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
124 if length $root_password < $min_length;
130 # check and setup @actions
131 my @actions = grep $_, split /,/, $args{'action'};
132 if ( @actions > 1 && $args{'datafile'} ) {
133 print STDERR "You can not use --datafile option with multiple actions.\n";
136 foreach ( @actions ) {
137 unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
138 print STDERR "$0 called with an invalid --action parameter.\n";
141 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
142 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
147 # convert init to multiple actions
149 if ( $actions[0] eq 'init' ) {
150 if ($args{'skip-create'}) {
151 @actions = qw(schema coredata insert);
153 @actions = qw(create schema acl coredata insert);
158 # set options from environment
159 foreach my $key(qw(Type Host Name User Password)) {
160 next unless exists $ENV{ 'RT_DB_'. uc $key };
161 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
162 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
165 my $db_type = RT->Config->Get('DatabaseType') || '';
166 my $db_host = RT->Config->Get('DatabaseHost') || '';
167 my $db_name = RT->Config->Get('DatabaseName') || '';
168 my $db_user = RT->Config->Get('DatabaseUser') || '';
169 my $db_pass = RT->Config->Get('DatabasePassword') || '';
171 # load it here to get error immidiatly if DB type is not supported
174 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
175 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
176 RT->Config->Set( DatabaseName => $db_name );
179 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
180 my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
182 if ($args{'skip-create'}) {
183 $dba_user = $db_user;
184 $dba_pass = $db_pass;
186 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
187 $dba_pass = get_dba_password();
188 chomp $dba_pass if defined($dba_pass);
192 print "Working with:\n"
193 ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
194 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
196 foreach my $action ( @actions ) {
198 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
199 error($action, $msg) unless $status;
200 print $msg .".\n" if $msg;
206 my $dbh = get_system_dbh();
207 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
208 return ($status, $msg) unless $status;
210 print "Now creating a $db_type database $db_name for RT.\n";
211 return RT::Handle->CreateDatabase( $dbh );
217 print "Dropping $db_type database $db_name.\n";
218 unless ( $args{'force'} ) {
221 About to drop $db_type database $db_name on $db_host.
222 WARNING: This will erase all data in $db_name.
225 exit(-2) unless _yesno();
228 my $dbh = get_system_dbh();
229 return RT::Handle->DropDatabase( $dbh );
234 my $dbh = get_admin_dbh();
235 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
236 return ($status, $msg) unless $status;
238 print "Now populating database schema.\n";
239 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
244 my $dbh = get_admin_dbh();
245 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
246 return ($status, $msg) unless $status;
248 print "Now inserting database ACLs.\n";
249 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
252 sub action_coredata {
254 $RT::Handle = RT::Handle->new;
255 $RT::Handle->dbh( undef );
256 RT::ConnectToDatabase();
258 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
259 return ($status, $msg) unless $status;
261 print "Now inserting RT core system objects.\n";
262 return $RT::Handle->InsertInitialData;
267 $RT::Handle = RT::Handle->new;
269 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
270 return ($status, $msg) unless $status;
272 print "Now inserting data.\n";
273 my $file = $args{'datafile'};
274 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
275 $file ||= $args{'datadir'}."/content";
277 # Slurp in backcompat
279 my @back = @{$args{backcompat} || []};
281 my @lines = do {local @ARGV = @back; <>};
285 my ($class, @fields) = split;
286 $class->_BuildTableAttributes;
287 $RT::Logger->debug("Temporarily removing @fields from $class");
288 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
293 my @ret = $RT::Handle->InsertData( $file, $root_password );
295 # Put back the fields we chopped off
296 for my $class (keys %removed) {
297 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
298 for keys %{$removed{$class}};
305 my $base_dir = $args{'datadir'} || "./etc/upgrade";
306 return (0, "Couldn't read dir '$base_dir' with upgrade data")
307 unless -d $base_dir || -r _;
309 my $version_word_regex = join '|', RT::Handle->version_words;
310 my $upgrading_from = undef;
312 if ( defined $upgrading_from ) {
313 print "Doesn't match #.#.#: ";
315 print "Enter RT version you're upgrading from: ";
317 $upgrading_from = scalar <STDIN>;
318 chomp $upgrading_from;
319 $upgrading_from =~ s/\s+//g;
320 } while $upgrading_from !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
322 my $upgrading_to = $RT::VERSION;
323 return (0, "The current version $upgrading_to is lower than $upgrading_from")
324 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
326 return (1, "The version $upgrading_to you're upgrading to is up to date")
327 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
329 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
330 return (1, "No DB changes since $upgrading_from")
333 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
334 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
335 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
336 $upgrading_to = $versions[-1];
339 print "\nGoing to apply following upgrades:\n";
340 print map "* $_\n", @versions;
343 my $custom_upgrading_to = undef;
345 if ( defined $custom_upgrading_to ) {
346 print "Doesn't match #.#.#: ";
348 print "\nEnter RT version if you want to stop upgrade at some point,\n";
349 print " or leave it blank if you want apply above upgrades: ";
351 $custom_upgrading_to = scalar <STDIN>;
352 chomp $custom_upgrading_to;
353 $custom_upgrading_to =~ s/\s+//g;
354 last unless $custom_upgrading_to;
355 } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
357 if ( $custom_upgrading_to ) {
359 0, "The version you entered ($custom_upgrading_to) is lower than\n"
360 ."version you're upgrading from ($upgrading_from)"
361 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
363 return (1, "The version you're upgrading to is up to date")
364 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
366 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
367 print "Version you entered is greater than installed ($RT::VERSION).\n";
368 _yesno() or exit(-2);
370 # ok, checked everything no let's refresh list
371 $upgrading_to = $custom_upgrading_to;
372 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
374 return (1, "No DB changes between $upgrading_from and $upgrading_to")
377 print "\nGoing to apply following upgrades:\n";
378 print map "* $_\n", @versions;
382 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
383 _yesno() or exit(-2) unless $args{'force'};
386 foreach my $n ( 0..$#versions ) {
387 my $v = $versions[$n];
388 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
389 print "Processing $v\n";
390 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
391 if ( -e "$base_dir/$v/schema.$db_type" ) {
392 ( $ret, $msg ) = action_schema( %tmp );
393 return ( $ret, $msg ) unless $ret;
395 if ( -e "$base_dir/$v/acl.$db_type" ) {
396 ( $ret, $msg ) = action_acl( %tmp );
397 return ( $ret, $msg ) unless $ret;
399 if ( -e "$base_dir/$v/content" ) {
400 ( $ret, $msg ) = action_insert( %tmp );
401 return ( $ret, $msg ) unless $ret;
407 sub get_versions_from_to {
408 my ($base_dir, $from, $to) = @_;
410 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
411 my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
415 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
416 grep RT::Handle::cmp_version($_, $from) > 0,
417 sort RT::Handle::cmp_version @versions;
421 my ($action, $msg) = @_;
422 print STDERR "Couldn't finish '$action' step.\n\n";
423 print STDERR "ERROR: $msg\n\n";
427 sub get_dba_password {
428 print "In order to create or update your RT database,"
429 . " this script needs to connect to your "
430 . " $db_type instance on $db_host as $dba_user\n";
431 print "Please specify that user's database password below. If the user has no database\n";
432 print "password, just press return.\n\n";
435 my $password = ReadLine(0);
442 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
443 # See also L<RT::Handle/SystemDSN>.
447 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
451 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
454 # get_rt_dbh [USER, PASSWORD]
456 # Returns L<DBI> database handle connected to RT database,
457 # you may specify credentials(USER and PASSWORD) to connect
458 # with. By default connects with credentials from RT config.
461 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
465 my ($dsn, $user, $pass) = @_;
466 my $dbh = DBI->connect(
468 { RaiseError => 0, PrintError => 0 },
471 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
472 if ( $args{'debug'} ) {
473 require Carp; Carp::confess( $msg );
475 print STDERR $msg; exit -1;
482 print "Proceed [y/N]:";
483 my $x = scalar(<STDIN>);
493 rt-setup-database - Set up RT's database
497 rt-setup-database --action ...
505 Several actions can be combined using comma separated list.
511 Initialize the database. This is combination of multiple actions listed below.
512 Create DB, schema, setup acl, insert core data and initial data.
516 Apply all needed schema/acl/content updates (will ask for version to upgrade
525 Drop the database. This will B<ERASE ALL YOUR DATA>.
529 Initialize only the database schema
531 To use a local or supplementary datafile, specify it using the '--datadir'
536 Initialize only the database ACLs
538 To use a local or supplementary datafile, specify it using the '--datadir'
543 Insert data into RT's database. This data is required for normal functioning of
548 Insert data into RT's database. By default, will use RT's installation data.
549 To use a local or supplementary datafile, specify it using the '--datafile'
556 file path of the data you want to action on
558 e.g. C<--datafile /path/to/datafile>
562 Used to specify a path to find the local database schema and acls to be
565 e.g. C<--datadir /path/to/>
575 =item prompt-for-dba-password
577 Ask for the database administrator's password interactively
581 for 'init': skip creating the database and the user account, so we don't need
582 administrator privileges
584 =item root-password-file
586 for 'init' and 'insert': rather than using the default administrative password
587 for RT's "root" user, use the password in this file.