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 = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
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 = exists($args{'dba-password'})
181 ? $args{'dba-password'}
182 : $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 print "Working with:\n"
195 ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
196 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
198 foreach my $action ( @actions ) {
200 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
201 error($action, $msg) unless $status;
202 print $msg .".\n" if $msg;
208 my $dbh = get_system_dbh();
209 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
210 return ($status, $msg) unless $status;
212 print "Now creating a $db_type database $db_name for RT.\n";
213 return RT::Handle->CreateDatabase( $dbh );
219 print "Dropping $db_type database $db_name.\n";
220 unless ( $args{'force'} ) {
223 About to drop $db_type database $db_name on $db_host.
224 WARNING: This will erase all data in $db_name.
227 exit(-2) unless _yesno();
230 my $dbh = get_system_dbh();
231 return RT::Handle->DropDatabase( $dbh );
236 my $dbh = get_admin_dbh();
237 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
238 return ($status, $msg) unless $status;
240 print "Now populating database schema.\n";
241 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
246 my $dbh = get_admin_dbh();
247 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
248 return ($status, $msg) unless $status;
250 print "Now inserting database ACLs.\n";
251 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
254 sub action_coredata {
256 $RT::Handle = RT::Handle->new;
257 $RT::Handle->dbh( undef );
258 RT::ConnectToDatabase();
260 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
261 return ($status, $msg) unless $status;
263 print "Now inserting RT core system objects.\n";
264 return $RT::Handle->InsertInitialData;
269 $RT::Handle = RT::Handle->new;
271 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
272 return ($status, $msg) unless $status;
274 print "Now inserting data.\n";
275 my $file = $args{'datafile'};
276 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
277 $file ||= $args{'datadir'}."/content";
279 # Slurp in backcompat
281 my @back = @{$args{backcompat} || []};
283 my @lines = do {local @ARGV = @back; <>};
287 my ($class, @fields) = split;
288 $class->_BuildTableAttributes;
289 $RT::Logger->debug("Temporarily removing @fields from $class");
290 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
295 my @ret = $RT::Handle->InsertData( $file, $root_password );
297 # Put back the fields we chopped off
298 for my $class (keys %removed) {
299 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
300 for keys %{$removed{$class}};
307 my $base_dir = $args{'datadir'} || "./etc/upgrade";
308 return (0, "Couldn't read dir '$base_dir' with upgrade data")
309 unless -d $base_dir || -r _;
311 my $version_word_regex = join '|', RT::Handle->version_words;
312 my $upgrading_from = undef;
314 if ( defined $upgrading_from ) {
315 print "Doesn't match #.#.#: ";
317 print "Enter RT version you're upgrading from: ";
319 $upgrading_from = scalar <STDIN>;
320 chomp $upgrading_from;
321 $upgrading_from =~ s/\s+//g;
322 } while $upgrading_from !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
324 my $upgrading_to = $RT::VERSION;
325 return (0, "The current version $upgrading_to is lower than $upgrading_from")
326 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
328 return (1, "The version $upgrading_to you're upgrading to is up to date")
329 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
331 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
332 return (1, "No DB changes since $upgrading_from")
335 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
336 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
337 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
338 $upgrading_to = $versions[-1];
341 print "\nGoing to apply following upgrades:\n";
342 print map "* $_\n", @versions;
345 my $custom_upgrading_to = undef;
347 if ( defined $custom_upgrading_to ) {
348 print "Doesn't match #.#.#: ";
350 print "\nEnter RT version if you want to stop upgrade at some point,\n";
351 print " or leave it blank if you want apply above upgrades: ";
353 $custom_upgrading_to = scalar <STDIN>;
354 chomp $custom_upgrading_to;
355 $custom_upgrading_to =~ s/\s+//g;
356 last unless $custom_upgrading_to;
357 } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
359 if ( $custom_upgrading_to ) {
361 0, "The version you entered ($custom_upgrading_to) is lower than\n"
362 ."version you're upgrading from ($upgrading_from)"
363 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
365 return (1, "The version you're upgrading to is up to date")
366 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
368 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
369 print "Version you entered is greater than installed ($RT::VERSION).\n";
370 _yesno() or exit(-2);
372 # ok, checked everything no let's refresh list
373 $upgrading_to = $custom_upgrading_to;
374 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
376 return (1, "No DB changes between $upgrading_from and $upgrading_to")
379 print "\nGoing to apply following upgrades:\n";
380 print map "* $_\n", @versions;
384 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
385 _yesno() or exit(-2) unless $args{'force'};
388 foreach my $n ( 0..$#versions ) {
389 my $v = $versions[$n];
390 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
391 print "Processing $v\n";
392 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
393 if ( -e "$base_dir/$v/schema.$db_type" ) {
394 ( $ret, $msg ) = action_schema( %tmp );
395 return ( $ret, $msg ) unless $ret;
397 if ( -e "$base_dir/$v/acl.$db_type" ) {
398 ( $ret, $msg ) = action_acl( %tmp );
399 return ( $ret, $msg ) unless $ret;
401 if ( -e "$base_dir/$v/content" ) {
402 ( $ret, $msg ) = action_insert( %tmp );
403 return ( $ret, $msg ) unless $ret;
409 sub get_versions_from_to {
410 my ($base_dir, $from, $to) = @_;
412 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
413 my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
417 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
418 grep RT::Handle::cmp_version($_, $from) > 0,
419 sort RT::Handle::cmp_version @versions;
423 my ($action, $msg) = @_;
424 print STDERR "Couldn't finish '$action' step.\n\n";
425 print STDERR "ERROR: $msg\n\n";
429 sub get_dba_password {
430 print "In order to create or update your RT database,"
431 . " this script needs to connect to your "
432 . " $db_type instance on $db_host as $dba_user\n";
433 print "Please specify that user's database password below. If the user has no database\n";
434 print "password, just press return.\n\n";
437 my $password = ReadLine(0);
444 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
445 # See also L<RT::Handle/SystemDSN>.
449 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
453 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
456 # get_rt_dbh [USER, PASSWORD]
458 # Returns L<DBI> database handle connected to RT database,
459 # you may specify credentials(USER and PASSWORD) to connect
460 # with. By default connects with credentials from RT config.
463 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
467 my ($dsn, $user, $pass) = @_;
468 my $dbh = DBI->connect(
470 { RaiseError => 0, PrintError => 0 },
473 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
474 if ( $args{'debug'} ) {
475 require Carp; Carp::confess( $msg );
477 print STDERR $msg; exit -1;
484 print "Proceed [y/N]:";
485 my $x = scalar(<STDIN>);
495 rt-setup-database - Set up RT's database
499 rt-setup-database --action ...
507 Several actions can be combined using comma separated list.
513 Initialize the database. This is combination of multiple actions listed below.
514 Create DB, schema, setup acl, insert core data and initial data.
518 Apply all needed schema/acl/content updates (will ask for version to upgrade
527 Drop the database. This will B<ERASE ALL YOUR DATA>.
531 Initialize only the database schema
533 To use a local or supplementary datafile, specify it using the '--datadir'
538 Initialize only the database ACLs
540 To use a local or supplementary datafile, specify it using the '--datadir'
545 Insert data into RT's database. This data is required for normal functioning of
550 Insert data into RT's database. By default, will use RT's installation data.
551 To use a local or supplementary datafile, specify it using the '--datafile'
558 file path of the data you want to action on
560 e.g. C<--datafile /path/to/datafile>
564 Used to specify a path to find the local database schema and acls to be
567 e.g. C<--datadir /path/to/>
577 =item prompt-for-dba-password
579 Ask for the database administrator's password interactively
583 for 'init': skip creating the database and the user account, so we don't need
584 administrator privileges
586 =item root-password-file
588 for 'init' and 'insert': rather than using the default administrative password
589 for RT's "root" user, use the password in this file.