2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2019 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 }}}
53 use vars qw($Nobody $SystemUser $item);
55 # fix lib paths, some may be relative
56 BEGIN { # BEGIN RT CMD BOILERPLATE
59 my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
63 unless ( File::Spec->file_name_is_absolute($lib) ) {
64 $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
65 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
76 $| = 1; # unbuffer all output.
85 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
86 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
87 'package=s', 'ext-version=s',
88 'upgrade-from=s', 'upgrade-to=s',
93 if ( $args{help} || ! $args{'action'} ) {
95 Pod::Usage::pod2usage({ verbose => 2 });
103 # Force warnings to be output to STDERR if we're not already logging
104 # them at a higher level
105 RT->Config->Set( LogToSTDERR => 'warning')
106 unless ( RT->Config->Get( 'LogToSTDERR' )
107 && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ );
110 # get customized root password
112 if ( $args{'root-password-file'} ) {
113 open( my $fh, '<', $args{'root-password-file'} )
114 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
115 $root_password = <$fh>;
116 chomp $root_password;
117 my $min_length = RT->Config->Get('MinimumPasswordLength');
120 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
121 if length $root_password < $min_length;
127 # check and setup @actions
128 my @actions = grep $_, split /,/, $args{'action'};
129 if ( @actions > 1 && $args{'datafile'} ) {
130 print STDERR "You can not use --datafile option with multiple actions.\n";
133 foreach ( @actions ) {
134 unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) {
135 print STDERR "$0 called with an invalid --action parameter.\n";
138 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
139 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
144 # convert init to multiple actions
146 if ( $actions[0] eq 'init' ) {
147 if ($args{'skip-create'}) {
148 @actions = qw(schema coredata insert);
150 @actions = qw(create schema acl coredata insert);
155 # set options from environment
156 foreach my $key(qw(Type Host Name User Password)) {
157 next unless exists $ENV{ 'RT_DB_'. uc $key };
158 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
159 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
162 my $db_type = RT->Config->Get('DatabaseType') || '';
163 my $db_host = RT->Config->Get('DatabaseHost') || '';
164 my $db_port = RT->Config->Get('DatabasePort') || '';
165 my $db_name = RT->Config->Get('DatabaseName') || '';
166 my $db_user = RT->Config->Get('DatabaseUser') || '';
167 my $db_pass = RT->Config->Get('DatabasePassword') || '';
169 # load it here to get error immidiatly if DB type is not supported
172 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
173 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
174 RT->Config->Set( DatabaseName => $db_name );
177 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
178 my $dba_pass = exists($args{'dba-password'})
179 ? $args{'dba-password'}
180 : $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 my $version_word_regex = join '|', RT::Handle->version_words;
193 my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
195 print "Working with:\n"
196 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
197 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
199 my $package = $args{'package'} || 'RT';
200 my $ext_version = $args{'ext-version'};
201 my $full_id = Data::GUID->new->as_string;
204 if ($args{'package'} ne 'RT') {
205 RT->ConnectToDatabase();
206 RT->InitSystemObjects();
210 foreach my $action ( @actions ) {
212 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
213 error($action, $msg) unless $status;
214 print $msg .".\n" if $msg;
220 my $dbh = get_system_dbh();
221 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
222 return ($status, $msg) unless $status;
224 print "Now creating a $db_type database $db_name for RT.\n";
225 return RT::Handle->CreateDatabase( $dbh );
231 print "Dropping $db_type database $db_name.\n";
232 unless ( $args{'force'} ) {
235 About to drop $db_type database $db_name on $db_host (port '$db_port').
236 WARNING: This will erase all data in $db_name.
239 exit(-2) unless _yesno();
242 my $dbh = get_system_dbh();
243 return RT::Handle->DropDatabase( $dbh );
248 my $dbh = get_admin_dbh();
249 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
250 return ($status, $msg) unless $status;
252 my $individual_id = Data::GUID->new->as_string();
255 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
258 individual_id => $individual_id,
260 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
261 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
263 print "Now populating database schema.\n";
264 my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
268 individual_id => $individual_id,
269 return_value => [ @ret ],
271 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
278 my $dbh = get_admin_dbh();
279 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
280 return ($status, $msg) unless $status;
282 my $individual_id = Data::GUID->new->as_string();
285 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
288 individual_id => $individual_id,
290 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
291 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
293 print "Now inserting database ACLs.\n";
294 my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
298 individual_id => $individual_id,
299 return_value => [ @ret ],
301 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
308 RT->ConnectToDatabase;
309 my $individual_id = Data::GUID->new->as_string();
312 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
315 individual_id => $individual_id,
317 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
318 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
320 my $dbh = get_admin_dbh();
321 $RT::Handle = RT::Handle->new;
322 $RT::Handle->dbh( $dbh );
325 print "Now inserting database indexes.\n";
326 my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} );
328 $RT::Handle = RT::Handle->new;
329 $RT::Handle->dbh( undef );
330 RT->ConnectToDatabase;
333 individual_id => $individual_id,
334 return_value => [ @ret ],
336 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
341 sub action_coredata {
343 $RT::Handle = RT::Handle->new;
344 $RT::Handle->dbh( undef );
345 RT::ConnectToDatabase();
346 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
347 return ($status, $msg) unless $status;
349 print "Now inserting RT core system objects.\n";
350 return $RT::Handle->InsertInitialData;
357 $RT::Handle = RT::Handle->new;
363 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
364 return ($status, $msg) unless $status;
366 print "Now inserting data.\n";
367 my $file = $args{'datafile'};
368 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
369 $file ||= $args{'datadir'}."/content";
371 my $individual_id = Data::GUID->new->as_string();
374 filename => Cwd::abs_path($file),
377 individual_id => $individual_id
379 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
381 open my $handle, '<', $file or warn "Unable to open $file: $!";
382 $upgrade_data{content} = do {local $/; <$handle>} if $handle;
384 RT->System->AddUpgradeHistory($package => \%upgrade_data);
388 my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) };
390 for my $file (@{$args{backcompat} || []}) {
391 my $lines = do {local $/; local @ARGV = ($file); <>};
392 my $sub = eval "sub {\n# line 1 $file\n$lines\n}";
394 warn "Failed to load backcompat $file: $@";
397 my $current = $upgrade;
398 $upgrade = sub { $sub->($current) };
403 # XXX Reconnecting to insert the history entry
404 # until we can sort out removing
405 # the disconnect at the end of InsertData.
406 RT->ConnectToDatabase();
410 individual_id => $individual_id,
411 return_value => [ @ret ],
414 RT->System->AddUpgradeHistory($package => \%upgrade_data);
416 my $db_type = RT->Config->Get('DatabaseType');
417 $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
424 my $base_dir = $args{'datadir'} || "./etc/upgrade";
425 return (0, "Couldn't read dir '$base_dir' with upgrade data")
426 unless -d $base_dir || -r _;
428 my $upgrading_from = undef;
430 if ( defined $upgrading_from ) {
431 print "Doesn't match #.#.#: ";
433 print "Enter $args{package} version you're upgrading from: ";
435 $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>;
436 chomp $upgrading_from;
437 $upgrading_from =~ s/\s+//g;
438 } while $upgrading_from !~ /$version_dir/;
440 my $upgrading_to = $RT::VERSION;
441 return (0, "The current version $upgrading_to is lower than $upgrading_from")
442 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
444 return (1, "The version $upgrading_to you're upgrading to is up to date")
445 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
447 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
448 return (1, "No DB changes since $upgrading_from")
451 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
452 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
453 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
454 $upgrading_to = $versions[-1];
457 print "\nGoing to apply following upgrades:\n";
458 print map "* $_\n", @versions;
461 my $custom_upgrading_to = undef;
463 if ( defined $custom_upgrading_to ) {
464 print "Doesn't match #.#.#: ";
466 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
467 print " or leave it blank if you want apply above upgrades: ";
469 $custom_upgrading_to = $args{'upgrade-to'} || scalar <STDIN>;
470 chomp $custom_upgrading_to;
471 $custom_upgrading_to =~ s/\s+//g;
472 last unless $custom_upgrading_to;
473 } while $custom_upgrading_to !~ /$version_dir/;
475 if ( $custom_upgrading_to ) {
477 0, "The version you entered ($custom_upgrading_to) is lower than\n"
478 ."version you're upgrading from ($upgrading_from)"
479 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
481 return (1, "The version you're upgrading to is up to date")
482 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
484 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
485 print "Version you entered is greater than installed ($RT::VERSION).\n";
486 _yesno() or exit(-2);
488 # ok, checked everything no let's refresh list
489 $upgrading_to = $custom_upgrading_to;
490 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
492 return (1, "No DB changes between $upgrading_from and $upgrading_to")
495 print "\nGoing to apply following upgrades:\n";
496 print map "* $_\n", @versions;
500 unless ( $args{'force'} ) {
501 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
502 _yesno() or exit(-2);
505 RT->ConnectToDatabase();
506 RT->InitSystemObjects();
509 RT->System->AddUpgradeHistory($package => {
510 type => 'full upgrade',
513 from => $upgrading_from,
515 versions => [@versions],
517 individual_id => $full_id
520 # Ensure that the Attributes column is big enough to hold the
521 # upgrade steps we're going to add; this step exists in 4.0.6 for
522 # mysql, but that may be too late. Run it as soon as possible.
523 if (RT->Config->Get('DatabaseType') eq 'mysql'
524 and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) {
525 my $dbh = get_admin_dbh();
526 # Before the binary switch in 3.7.87, we want to alter text ->
527 # longtext, not blob -> longblob
528 if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) {
529 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT")
531 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB")
535 my $previous = $upgrading_from;
537 foreach my $n ( 0..$#versions ) {
538 my $v = $versions[$n];
539 my $individual_id = Data::GUID->new->as_string();
541 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
542 print "Processing $v\n";
544 RT->System->AddUpgradeHistory($package => {
546 type => 'individual upgrade',
551 individual_id => $individual_id,
554 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
556 if ( -e "$base_dir/$v/schema.$db_type" ) {
557 ( $ret, $msg ) = action_schema( %tmp );
558 return ( $ret, $msg ) unless $ret;
560 if ( -e "$base_dir/$v/acl.$db_type" ) {
561 ( $ret, $msg ) = action_acl( %tmp );
562 return ( $ret, $msg ) unless $ret;
564 if ( -e "$base_dir/$v/indexes" ) {
565 ( $ret, $msg ) = action_indexes( %tmp );
566 return ( $ret, $msg ) unless $ret;
568 if ( -e "$base_dir/$v/content" ) {
569 ( $ret, $msg ) = action_insert( %tmp );
570 return ( $ret, $msg ) unless $ret;
573 # XXX: Another connect since the insert called
574 # previous to this step will disconnect.
576 RT->ConnectToDatabase();
578 RT->System->AddUpgradeHistory($package => {
580 individual_id => $individual_id,
586 RT->System->AddUpgradeHistory($package => {
588 individual_id => $full_id,
594 sub get_versions_from_to {
595 my ($base_dir, $from, $to) = @_;
597 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
598 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
601 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
605 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
606 grep RT::Handle::cmp_version($_, $from) > 0,
607 sort RT::Handle::cmp_version @versions;
611 my ($action, $msg) = @_;
612 print STDERR "Couldn't finish '$action' step.\n\n";
613 print STDERR "ERROR: $msg\n\n";
617 sub get_dba_password {
618 print "In order to create or update your RT database,"
619 . " this script needs to connect to your "
620 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
621 print "Please specify that user's database password below. If the user has no database\n";
622 print "password, just press return.\n\n";
625 my $password = ReadLine(0);
632 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
633 # See also L<RT::Handle/SystemDSN>.
637 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
641 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
644 # get_rt_dbh [USER, PASSWORD]
646 # Returns L<DBI> database handle connected to RT database,
647 # you may specify credentials(USER and PASSWORD) to connect
648 # with. By default connects with credentials from RT config.
651 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
655 my ($dsn, $user, $pass) = @_;
656 my $dbh = DBI->connect(
658 { RaiseError => 0, PrintError => 0 },
661 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
662 if ( $args{'debug'} ) {
663 require Carp; Carp::confess( $msg );
665 print STDERR $msg; exit -1;
672 print "Proceed [y/N]:";
673 my $x = scalar(<STDIN>);
683 rt-setup-database - Set up RT's database
687 rt-setup-database --action ...
695 Several actions can be combined using comma separated list.
701 Initialize the database. This is combination of multiple actions listed below.
702 Create DB, schema, setup acl, insert core data and initial data.
706 Apply all needed schema/acl/content updates (will ask for version to upgrade
715 Drop the database. This will B<ERASE ALL YOUR DATA>.
719 Initialize only the database schema
721 To use a local or supplementary datafile, specify it using the '--datadir'
726 Initialize only the database ACLs
728 To use a local or supplementary datafile, specify it using the '--datadir'
733 Insert data into RT's database. This data is required for normal functioning of
738 Insert data into RT's database. By default, will use RT's installation data.
739 To use a local or supplementary datafile, specify it using the '--datafile'
746 file path of the data you want to action on
748 e.g. C<--datafile /path/to/datafile>
752 Used to specify a path to find the local database schema and acls to be
755 e.g. C<--datadir /path/to/>
765 =item prompt-for-dba-password
767 Ask for the database administrator's password interactively
771 for 'init': skip creating the database and the user account, so we don't need
772 administrator privileges
774 =item root-password-file
776 for 'init' and 'insert': rather than using the default administrative password
777 for RT's "root" user, use the password in this file.
781 the name of the entity performing a create or upgrade. Used for logging changes
782 in the DB. Defaults to RT, otherwise it should be the fully qualified package name
783 of the extension or plugin making changes to the DB.
787 current version of extension making a change. Not needed for RT since RT has a
788 more elaborate system to track upgrades across multiple versions.
792 for 'upgrade': specifies the version to upgrade from, and do not prompt
793 for it if it appears to be a valid version.
797 for 'upgrade': specifies the version to upgrade to, and do not prompt
798 for it if it appears to be a valid version.