2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2015 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.
92 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
93 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
94 'upgrade-from=s', 'upgrade-to=s',
99 if ( $args{help} || ! $args{'action'} ) {
101 Pod::Usage::pod2usage({ verbose => 2 });
109 # Force warnings to be output to STDERR if we're not already logging
110 # them at a higher level
111 RT->Config->Set( LogToScreen => 'warning')
112 unless ( RT->Config->Get( 'LogToScreen' )
113 && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ );
115 # get customized root password
117 if ( $args{'root-password-file'} ) {
118 open( my $fh, '<', $args{'root-password-file'} )
119 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
120 $root_password = <$fh>;
121 chomp $root_password;
122 my $min_length = RT->Config->Get('MinimumPasswordLength');
125 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
126 if length $root_password < $min_length;
132 # check and setup @actions
133 my @actions = grep $_, split /,/, $args{'action'};
134 if ( @actions > 1 && $args{'datafile'} ) {
135 print STDERR "You can not use --datafile option with multiple actions.\n";
138 foreach ( @actions ) {
139 unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
140 print STDERR "$0 called with an invalid --action parameter.\n";
143 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
144 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
149 # convert init to multiple actions
151 if ( $actions[0] eq 'init' ) {
152 if ($args{'skip-create'}) {
153 @actions = qw(schema coredata insert);
155 @actions = qw(create schema acl coredata insert);
160 # set options from environment
161 foreach my $key(qw(Type Host Name User Password)) {
162 next unless exists $ENV{ 'RT_DB_'. uc $key };
163 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
164 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
167 my $db_type = RT->Config->Get('DatabaseType') || '';
168 my $db_host = RT->Config->Get('DatabaseHost') || '';
169 my $db_port = RT->Config->Get('DatabasePort') || '';
170 my $db_name = RT->Config->Get('DatabaseName') || '';
171 my $db_user = RT->Config->Get('DatabaseUser') || '';
172 my $db_pass = RT->Config->Get('DatabasePassword') || '';
174 # load it here to get error immidiatly if DB type is not supported
177 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
178 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
179 RT->Config->Set( DatabaseName => $db_name );
182 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
183 my $dba_pass = exists($args{'dba-password'})
184 ? $args{'dba-password'}
185 : $ENV{'RT_DBA_PASSWORD'};
187 if ($args{'skip-create'}) {
188 $dba_user = $db_user;
189 $dba_pass = $db_pass;
191 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
192 $dba_pass = get_dba_password();
193 chomp $dba_pass if defined($dba_pass);
197 my $version_word_regex = join '|', RT::Handle->version_words;
198 my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
200 print "Working with:\n"
201 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
202 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
204 foreach my $action ( @actions ) {
206 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
207 error($action, $msg) unless $status;
208 print $msg .".\n" if $msg;
214 my $dbh = get_system_dbh();
215 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
216 return ($status, $msg) unless $status;
218 print "Now creating a $db_type database $db_name for RT.\n";
219 return RT::Handle->CreateDatabase( $dbh );
225 print "Dropping $db_type database $db_name.\n";
226 unless ( $args{'force'} ) {
229 About to drop $db_type database $db_name on $db_host (port '$db_port').
230 WARNING: This will erase all data in $db_name.
233 exit(-2) unless _yesno();
236 my $dbh = get_system_dbh();
237 return RT::Handle->DropDatabase( $dbh );
242 my $dbh = get_admin_dbh();
243 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
244 return ($status, $msg) unless $status;
246 print "Now populating database schema.\n";
247 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
252 my $dbh = get_admin_dbh();
253 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
254 return ($status, $msg) unless $status;
256 print "Now inserting database ACLs.\n";
257 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
260 sub action_coredata {
262 $RT::Handle = RT::Handle->new;
263 $RT::Handle->dbh( undef );
264 RT::ConnectToDatabase();
266 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
267 return ($status, $msg) unless $status;
269 print "Now inserting RT core system objects.\n";
270 return $RT::Handle->InsertInitialData;
275 $RT::Handle = RT::Handle->new;
277 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
278 return ($status, $msg) unless $status;
280 print "Now inserting data.\n";
281 my $file = $args{'datafile'};
282 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
283 $file ||= $args{'datadir'}."/content";
285 # Slurp in backcompat
287 my @back = @{$args{backcompat} || []};
289 my @lines = do {local @ARGV = @back; <>};
293 my ($class, @fields) = split;
294 $class->_BuildTableAttributes;
295 $RT::Logger->debug("Temporarily removing @fields from $class");
296 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
301 my @ret = $RT::Handle->InsertData( $file, $root_password );
303 # Put back the fields we chopped off
304 for my $class (keys %removed) {
305 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
306 for keys %{$removed{$class}};
313 my $base_dir = $args{'datadir'} || "./etc/upgrade";
314 return (0, "Couldn't read dir '$base_dir' with upgrade data")
315 unless -d $base_dir || -r _;
317 my $upgrading_from = undef;
319 if ( defined $upgrading_from ) {
320 print "Doesn't match #.#.#: ";
322 print "Enter $args{package} version you're upgrading from: ";
324 $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>;
325 chomp $upgrading_from;
326 $upgrading_from =~ s/\s+//g;
327 } while $upgrading_from !~ /$version_dir/;
329 my $upgrading_to = $RT::VERSION;
330 return (0, "The current version $upgrading_to is lower than $upgrading_from")
331 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
333 return (1, "The version $upgrading_to you're upgrading to is up to date")
334 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
336 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
337 return (1, "No DB changes since $upgrading_from")
340 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
341 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
342 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
343 $upgrading_to = $versions[-1];
346 print "\nGoing to apply following upgrades:\n";
347 print map "* $_\n", @versions;
350 my $custom_upgrading_to = undef;
352 if ( defined $custom_upgrading_to ) {
353 print "Doesn't match #.#.#: ";
355 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
356 print " or leave it blank if you want apply above upgrades: ";
358 $custom_upgrading_to = $args{'upgrade-to'} || scalar <STDIN>;
359 chomp $custom_upgrading_to;
360 $custom_upgrading_to =~ s/\s+//g;
361 last unless $custom_upgrading_to;
362 } while $custom_upgrading_to !~ /$version_dir/;
364 if ( $custom_upgrading_to ) {
366 0, "The version you entered ($custom_upgrading_to) is lower than\n"
367 ."version you're upgrading from ($upgrading_from)"
368 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
370 return (1, "The version you're upgrading to is up to date")
371 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
373 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
374 print "Version you entered is greater than installed ($RT::VERSION).\n";
375 _yesno() or exit(-2);
377 # ok, checked everything no let's refresh list
378 $upgrading_to = $custom_upgrading_to;
379 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
381 return (1, "No DB changes between $upgrading_from and $upgrading_to")
384 print "\nGoing to apply following upgrades:\n";
385 print map "* $_\n", @versions;
389 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
390 _yesno() or exit(-2) unless $args{'force'};
393 foreach my $n ( 0..$#versions ) {
394 my $v = $versions[$n];
395 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
396 print "Processing $v\n";
397 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
398 if ( -e "$base_dir/$v/schema.$db_type" ) {
399 ( $ret, $msg ) = action_schema( %tmp );
400 return ( $ret, $msg ) unless $ret;
402 if ( -e "$base_dir/$v/acl.$db_type" ) {
403 ( $ret, $msg ) = action_acl( %tmp );
404 return ( $ret, $msg ) unless $ret;
406 if ( -e "$base_dir/$v/content" ) {
407 ( $ret, $msg ) = action_insert( %tmp );
408 return ( $ret, $msg ) unless $ret;
414 sub get_versions_from_to {
415 my ($base_dir, $from, $to) = @_;
417 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
418 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
421 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
425 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
426 grep RT::Handle::cmp_version($_, $from) > 0,
427 sort RT::Handle::cmp_version @versions;
431 my ($action, $msg) = @_;
432 print STDERR "Couldn't finish '$action' step.\n\n";
433 print STDERR "ERROR: $msg\n\n";
437 sub get_dba_password {
438 print "In order to create or update your RT database,"
439 . " this script needs to connect to your "
440 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
441 print "Please specify that user's database password below. If the user has no database\n";
442 print "password, just press return.\n\n";
445 my $password = ReadLine(0);
452 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
453 # See also L<RT::Handle/SystemDSN>.
457 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
461 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
464 # get_rt_dbh [USER, PASSWORD]
466 # Returns L<DBI> database handle connected to RT database,
467 # you may specify credentials(USER and PASSWORD) to connect
468 # with. By default connects with credentials from RT config.
471 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
475 my ($dsn, $user, $pass) = @_;
476 my $dbh = DBI->connect(
478 { RaiseError => 0, PrintError => 0 },
481 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
482 if ( $args{'debug'} ) {
483 require Carp; Carp::confess( $msg );
485 print STDERR $msg; exit -1;
492 print "Proceed [y/N]:";
493 my $x = scalar(<STDIN>);
503 rt-setup-database - Set up RT's database
507 rt-setup-database --action ...
515 Several actions can be combined using comma separated list.
521 Initialize the database. This is combination of multiple actions listed below.
522 Create DB, schema, setup acl, insert core data and initial data.
526 Apply all needed schema/acl/content updates (will ask for version to upgrade
535 Drop the database. This will B<ERASE ALL YOUR DATA>.
539 Initialize only the database schema
541 To use a local or supplementary datafile, specify it using the '--datadir'
546 Initialize only the database ACLs
548 To use a local or supplementary datafile, specify it using the '--datadir'
553 Insert data into RT's database. This data is required for normal functioning of
558 Insert data into RT's database. By default, will use RT's installation data.
559 To use a local or supplementary datafile, specify it using the '--datafile'
566 file path of the data you want to action on
568 e.g. C<--datafile /path/to/datafile>
572 Used to specify a path to find the local database schema and acls to be
575 e.g. C<--datadir /path/to/>
585 =item prompt-for-dba-password
587 Ask for the database administrator's password interactively
591 for 'init': skip creating the database and the user account, so we don't need
592 administrator privileges
594 =item root-password-file
596 for 'init' and 'insert': rather than using the default administrative password
597 for RT's "root" user, use the password in this file.
601 for 'upgrade': specifies the version to upgrade from, and do not prompt
602 for it if it appears to be a valid version.
606 for 'upgrade': specifies the version to upgrade to, and do not prompt
607 for it if it appears to be a valid version.