2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2012 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 = ("/opt/rt3/lib", "/opt/rt3/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 = 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, 'pre' );
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, 'pre' );
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, 'pre' );
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, 'pre' );
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, 'pre' );
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 $upgrading_from = undef;
313 if ( defined $upgrading_from ) {
314 print "Doesn't match #.#.#: ";
316 print "Enter RT version you're upgrading from: ";
318 $upgrading_from = scalar <STDIN>;
319 chomp $upgrading_from;
320 $upgrading_from =~ s/\s+//g;
321 } while $upgrading_from !~ /^\d+\.\d+\.\w+$/;
323 my $upgrading_to = $RT::VERSION;
324 return (0, "The current version $upgrading_to is lower than $upgrading_from")
325 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
327 return (1, "The version $upgrading_to you're upgrading to is up to date")
328 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
330 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
331 return (1, "No DB changes since $upgrading_from")
334 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
335 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
336 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
337 $upgrading_to = $versions[-1];
340 print "\nGoing to apply following upgrades:\n";
341 print map "* $_\n", @versions;
344 my $custom_upgrading_to = undef;
346 if ( defined $custom_upgrading_to ) {
347 print "Doesn't match #.#.#: ";
349 print "\nEnter RT version if you want to stop upgrade at some point,\n";
350 print " or leave it blank if you want apply above upgrades: ";
352 $custom_upgrading_to = scalar <STDIN>;
353 chomp $custom_upgrading_to;
354 $custom_upgrading_to =~ s/\s+//g;
355 last unless $custom_upgrading_to;
356 } while $custom_upgrading_to !~ /^\d+\.\d+\.\w+$/;
358 if ( $custom_upgrading_to ) {
360 0, "The version you entered ($custom_upgrading_to) is lower than\n"
361 ."version you're upgrading from ($upgrading_from)"
362 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
364 return (1, "The version you're upgrading to is up to date")
365 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
367 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
368 print "Version you entered is greater than installed ($RT::VERSION).\n";
369 _yesno() or exit(-2);
371 # ok, checked everything no let's refresh list
372 $upgrading_to = $custom_upgrading_to;
373 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
375 return (1, "No DB changes between $upgrading_from and $upgrading_to")
378 print "\nGoing to apply following upgrades:\n";
379 print map "* $_\n", @versions;
383 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
384 _yesno() or exit(-2) unless $args{'force'};
387 foreach my $n ( 0..$#versions ) {
388 my $v = $versions[$n];
389 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
390 print "Processing $v\n";
391 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
392 if ( -e "$base_dir/$v/schema.$db_type" ) {
393 ( $ret, $msg ) = action_schema( %tmp );
394 return ( $ret, $msg ) unless $ret;
396 if ( -e "$base_dir/$v/acl.$db_type" ) {
397 ( $ret, $msg ) = action_acl( %tmp );
398 return ( $ret, $msg ) unless $ret;
400 if ( -e "$base_dir/$v/content" ) {
401 ( $ret, $msg ) = action_insert( %tmp );
402 return ( $ret, $msg ) unless $ret;
408 sub get_versions_from_to {
409 my ($base_dir, $from, $to) = @_;
411 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
412 my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
416 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
417 grep RT::Handle::cmp_version($_, $from) > 0,
418 sort RT::Handle::cmp_version @versions;
422 my ($action, $msg) = @_;
423 print STDERR "Couldn't finish '$action' step.\n\n";
424 print STDERR "ERROR: $msg\n\n";
428 sub get_dba_password {
429 print "In order to create or update your RT database,"
430 . " this script needs to connect to your "
431 . " $db_type instance on $db_host as $dba_user\n";
432 print "Please specify that user's database password below. If the user has no database\n";
433 print "password, just press return.\n\n";
436 my $password = ReadLine(0);
443 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
444 # See also L<RT::Handle/SystemDSN>.
448 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
452 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
455 # get_rt_dbh [USER, PASSWORD]
457 # Returns L<DBI> database handle connected to RT database,
458 # you may specify credentials(USER and PASSWORD) to connect
459 # with. By default connects with credentials from RT config.
462 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
466 my ($dsn, $user, $pass) = @_;
467 my $dbh = DBI->connect(
469 { RaiseError => 0, PrintError => 0 },
472 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
473 if ( $args{'debug'} ) {
474 require Carp; Carp::confess( $msg );
476 print STDERR $msg; exit -1;
483 print "Proceed [y/N]:";
484 my $x = scalar(<STDIN>);
494 rt-setup-database - Set up RT's database
498 rt-setup-database --action ...
506 Several actions can be combined using comma separated list.
512 Initialize the database. This is combination of multiple actions listed below.
513 Create DB, schema, setup acl, insert core data and initial data.
517 Apply all needed schema/acl/content updates (will ask for version to upgrade
526 Drop the database. This will B<ERASE ALL YOUR DATA>.
530 Initialize only the database schema
532 To use a local or supplementary datafile, specify it using the '--datadir'
537 Initialize only the database ACLs
539 To use a local or supplementary datafile, specify it using the '--datadir'
544 Insert data into RT's database. This data is required for normal functioning of
549 Insert data into RT's database. By default, will use RT's installation data.
550 To use a local or supplementary datafile, specify it using the '--datafile'
557 file path of the data you want to action on
559 e.g. C<--datafile /path/to/datafile>
563 Used to specify a path to find the local database schema and acls to be
566 e.g. C<--datadir /path/to/>
576 =item prompt-for-dba-password
578 Ask for the database administrator's password interactively
582 for 'init': skip creating the database and the user account, so we don't need
583 administrator privileges
585 =item root-password-file
587 for 'init' and 'insert': rather than using the default administrative password
588 for RT's "root" user, use the password in this file.