2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2014 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
55 BEGIN { # BEGIN RT CMD BOILERPLATE
58 my @libs = ("lib", "local/lib");
62 unless ( File::Spec->file_name_is_absolute($lib) ) {
63 $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
64 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
75 $| = 1; # unbuffer all output.
84 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
85 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
86 'package=s', 'ext-version=s',
87 'upgrade-from=s', 'upgrade-to=s',
92 if ( $args{help} || ! $args{'action'} ) {
94 Pod::Usage::pod2usage({ verbose => 2 });
102 # Force warnings to be output to STDERR if we're not already logging
103 # them at a higher level
104 RT->Config->Set( LogToSTDERR => 'warning')
105 unless ( RT->Config->Get( 'LogToSTDERR' )
106 && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ );
109 # get customized root password
111 if ( $args{'root-password-file'} ) {
112 open( my $fh, '<', $args{'root-password-file'} )
113 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
114 $root_password = <$fh>;
115 chomp $root_password;
116 my $min_length = RT->Config->Get('MinimumPasswordLength');
119 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
120 if length $root_password < $min_length;
126 # check and setup @actions
127 my @actions = grep $_, split /,/, $args{'action'};
128 if ( @actions > 1 && $args{'datafile'} ) {
129 print STDERR "You can not use --datafile option with multiple actions.\n";
132 foreach ( @actions ) {
133 unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) {
134 print STDERR "$0 called with an invalid --action parameter.\n";
137 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
138 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
143 # convert init to multiple actions
145 if ( $actions[0] eq 'init' ) {
146 if ($args{'skip-create'}) {
147 @actions = qw(schema coredata insert);
149 @actions = qw(create schema acl coredata insert);
154 # set options from environment
155 foreach my $key(qw(Type Host Name User Password)) {
156 next unless exists $ENV{ 'RT_DB_'. uc $key };
157 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
158 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
161 my $db_type = RT->Config->Get('DatabaseType') || '';
162 my $db_host = RT->Config->Get('DatabaseHost') || '';
163 my $db_port = RT->Config->Get('DatabasePort') || '';
164 my $db_name = RT->Config->Get('DatabaseName') || '';
165 my $db_user = RT->Config->Get('DatabaseUser') || '';
166 my $db_pass = RT->Config->Get('DatabasePassword') || '';
168 # load it here to get error immidiatly if DB type is not supported
171 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
172 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
173 RT->Config->Set( DatabaseName => $db_name );
176 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || RT->Config->Get('DatabaseAdmin') || '';
177 my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
179 if ($args{'skip-create'}) {
180 $dba_user = $db_user;
181 $dba_pass = $db_pass;
183 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
184 $dba_pass = get_dba_password();
185 chomp $dba_pass if defined($dba_pass);
189 my $version_word_regex = join '|', RT::Handle->version_words;
190 my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
192 print "Working with:\n"
193 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
194 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
196 my $package = $args{'package'} || 'RT';
197 my $ext_version = $args{'ext-version'};
198 my $full_id = Data::GUID->new->as_string;
201 if ($args{'package'} ne 'RT') {
202 RT->ConnectToDatabase();
203 RT->InitSystemObjects();
207 foreach my $action ( @actions ) {
209 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
210 error($action, $msg) unless $status;
211 print $msg .".\n" if $msg;
217 my $dbh = get_system_dbh();
218 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
219 return ($status, $msg) unless $status;
221 print "Now creating a $db_type database $db_name for RT.\n";
222 return RT::Handle->CreateDatabase( $dbh );
228 print "Dropping $db_type database $db_name.\n";
229 unless ( $args{'force'} ) {
232 About to drop $db_type database $db_name on $db_host (port '$db_port').
233 WARNING: This will erase all data in $db_name.
236 exit(-2) unless _yesno();
239 my $dbh = get_system_dbh();
240 return RT::Handle->DropDatabase( $dbh );
245 my $dbh = get_admin_dbh();
246 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
247 return ($status, $msg) unless $status;
249 my $individual_id = Data::GUID->new->as_string();
252 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
255 individual_id => $individual_id,
257 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
258 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
260 print "Now populating database schema.\n";
261 my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
265 individual_id => $individual_id,
266 return_value => [ @ret ],
268 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
275 my $dbh = get_admin_dbh();
276 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
277 return ($status, $msg) unless $status;
279 my $individual_id = Data::GUID->new->as_string();
282 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
285 individual_id => $individual_id,
287 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
288 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
290 print "Now inserting database ACLs.\n";
291 my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
295 individual_id => $individual_id,
296 return_value => [ @ret ],
298 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
305 RT->ConnectToDatabase;
306 my $individual_id = Data::GUID->new->as_string();
309 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
312 individual_id => $individual_id,
314 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
315 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
317 my $dbh = get_admin_dbh();
318 $RT::Handle = RT::Handle->new;
319 $RT::Handle->dbh( $dbh );
322 print "Now inserting database indexes.\n";
323 my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} );
325 $RT::Handle = RT::Handle->new;
326 $RT::Handle->dbh( undef );
327 RT->ConnectToDatabase;
330 individual_id => $individual_id,
331 return_value => [ @ret ],
333 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
338 sub action_coredata {
340 $RT::Handle = RT::Handle->new;
341 $RT::Handle->dbh( undef );
342 RT::ConnectToDatabase();
343 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
344 return ($status, $msg) unless $status;
346 print "Now inserting RT core system objects.\n";
347 return $RT::Handle->InsertInitialData;
352 $RT::Handle = RT::Handle->new;
356 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
357 return ($status, $msg) unless $status;
359 print "Now inserting data.\n";
360 my $file = $args{'datafile'};
361 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
362 $file ||= $args{'datadir'}."/content";
364 my $individual_id = Data::GUID->new->as_string();
367 filename => Cwd::abs_path($file),
370 individual_id => $individual_id
372 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
374 open my $handle, '<', $file or warn "Unable to open $file: $!";
375 $upgrade_data{content} = do {local $/; <$handle>} if $handle;
377 RT->System->AddUpgradeHistory($package => \%upgrade_data);
381 my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) };
383 for my $file (@{$args{backcompat} || []}) {
384 my $lines = do {local $/; local @ARGV = ($file); <>};
385 my $sub = eval "sub {\n# line 1 $file\n$lines\n}";
387 warn "Failed to load backcompat $file: $@";
390 my $current = $upgrade;
391 $upgrade = sub { $sub->($current) };
396 # XXX Reconnecting to insert the history entry
397 # until we can sort out removing
398 # the disconnect at the end of InsertData.
399 RT->ConnectToDatabase();
403 individual_id => $individual_id,
404 return_value => [ @ret ],
407 RT->System->AddUpgradeHistory($package => \%upgrade_data);
409 my $db_type = RT->Config->Get('DatabaseType');
410 $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
417 my $base_dir = $args{'datadir'} || "./etc/upgrade";
418 return (0, "Couldn't read dir '$base_dir' with upgrade data")
419 unless -d $base_dir || -r _;
421 my $upgrading_from = undef;
423 if ( defined $upgrading_from ) {
424 print "Doesn't match #.#.#: ";
426 print "Enter $args{package} version you're upgrading from: ";
428 $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>;
429 chomp $upgrading_from;
430 $upgrading_from =~ s/\s+//g;
431 } while $upgrading_from !~ /$version_dir/;
433 my $upgrading_to = $RT::VERSION;
434 return (0, "The current version $upgrading_to is lower than $upgrading_from")
435 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
437 return (1, "The version $upgrading_to you're upgrading to is up to date")
438 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
440 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
441 return (1, "No DB changes since $upgrading_from")
444 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
445 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
446 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
447 $upgrading_to = $versions[-1];
450 print "\nGoing to apply following upgrades:\n";
451 print map "* $_\n", @versions;
454 my $custom_upgrading_to = undef;
456 if ( defined $custom_upgrading_to ) {
457 print "Doesn't match #.#.#: ";
459 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
460 print " or leave it blank if you want apply above upgrades: ";
462 $custom_upgrading_to = $args{'upgrade-to'} || scalar <STDIN>;
463 chomp $custom_upgrading_to;
464 $custom_upgrading_to =~ s/\s+//g;
465 last unless $custom_upgrading_to;
466 } while $custom_upgrading_to !~ /$version_dir/;
468 if ( $custom_upgrading_to ) {
470 0, "The version you entered ($custom_upgrading_to) is lower than\n"
471 ."version you're upgrading from ($upgrading_from)"
472 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
474 return (1, "The version you're upgrading to is up to date")
475 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
477 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
478 print "Version you entered is greater than installed ($RT::VERSION).\n";
479 _yesno() or exit(-2);
481 # ok, checked everything no let's refresh list
482 $upgrading_to = $custom_upgrading_to;
483 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
485 return (1, "No DB changes between $upgrading_from and $upgrading_to")
488 print "\nGoing to apply following upgrades:\n";
489 print map "* $_\n", @versions;
493 unless ( $args{'force'} ) {
494 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
495 _yesno() or exit(-2);
498 RT->ConnectToDatabase();
499 RT->InitSystemObjects();
502 RT->System->AddUpgradeHistory($package => {
503 type => 'full upgrade',
506 from => $upgrading_from,
508 versions => [@versions],
510 individual_id => $full_id
513 # Ensure that the Attributes column is big enough to hold the
514 # upgrade steps we're going to add; this step exists in 4.0.6 for
515 # mysql, but that may be too late. Run it as soon as possible.
516 if (RT->Config->Get('DatabaseType') eq 'mysql'
517 and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) {
518 my $dbh = get_admin_dbh();
519 # Before the binary switch in 3.7.87, we want to alter text ->
520 # longtext, not blob -> longblob
521 if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) {
522 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT")
524 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB")
528 my $previous = $upgrading_from;
530 foreach my $n ( 0..$#versions ) {
531 my $v = $versions[$n];
532 my $individual_id = Data::GUID->new->as_string();
534 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
535 print "Processing $v\n";
537 RT->System->AddUpgradeHistory($package => {
539 type => 'individual upgrade',
544 individual_id => $individual_id,
547 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
549 if ( -e "$base_dir/$v/schema.$db_type" ) {
550 ( $ret, $msg ) = action_schema( %tmp );
551 return ( $ret, $msg ) unless $ret;
553 if ( -e "$base_dir/$v/acl.$db_type" ) {
554 ( $ret, $msg ) = action_acl( %tmp );
555 return ( $ret, $msg ) unless $ret;
557 if ( -e "$base_dir/$v/indexes" ) {
558 ( $ret, $msg ) = action_indexes( %tmp );
559 return ( $ret, $msg ) unless $ret;
561 if ( -e "$base_dir/$v/content" ) {
562 ( $ret, $msg ) = action_insert( %tmp );
563 return ( $ret, $msg ) unless $ret;
566 # XXX: Another connect since the insert called
567 # previous to this step will disconnect.
569 RT->ConnectToDatabase();
571 RT->System->AddUpgradeHistory($package => {
573 individual_id => $individual_id,
579 RT->System->AddUpgradeHistory($package => {
581 individual_id => $full_id,
587 sub get_versions_from_to {
588 my ($base_dir, $from, $to) = @_;
590 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
591 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
594 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
598 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
599 grep RT::Handle::cmp_version($_, $from) > 0,
600 sort RT::Handle::cmp_version @versions;
604 my ($action, $msg) = @_;
605 print STDERR "Couldn't finish '$action' step.\n\n";
606 print STDERR "ERROR: $msg\n\n";
610 sub get_dba_password {
611 print "In order to create or update your RT database,"
612 . " this script needs to connect to your "
613 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
614 print "Please specify that user's database password below. If the user has no database\n";
615 print "password, just press return.\n\n";
618 my $password = ReadLine(0);
625 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
626 # See also L<RT::Handle/SystemDSN>.
630 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
634 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
637 # get_rt_dbh [USER, PASSWORD]
639 # Returns L<DBI> database handle connected to RT database,
640 # you may specify credentials(USER and PASSWORD) to connect
641 # with. By default connects with credentials from RT config.
644 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
648 my ($dsn, $user, $pass) = @_;
649 my $dbh = DBI->connect(
651 { RaiseError => 0, PrintError => 0 },
654 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
655 if ( $args{'debug'} ) {
656 require Carp; Carp::confess( $msg );
658 print STDERR $msg; exit -1;
665 print "Proceed [y/N]:";
666 my $x = scalar(<STDIN>);
676 rt-setup-database - Set up RT's database
680 rt-setup-database --action ...
688 Several actions can be combined using comma separated list.
694 Initialize the database. This is combination of multiple actions listed below.
695 Create DB, schema, setup acl, insert core data and initial data.
699 Apply all needed schema/acl/content updates (will ask for version to upgrade
708 Drop the database. This will B<ERASE ALL YOUR DATA>.
712 Initialize only the database schema
714 To use a local or supplementary datafile, specify it using the '--datadir'
719 Initialize only the database ACLs
721 To use a local or supplementary datafile, specify it using the '--datadir'
726 Insert data into RT's database. This data is required for normal functioning of
731 Insert data into RT's database. By default, will use RT's installation data.
732 To use a local or supplementary datafile, specify it using the '--datafile'
739 file path of the data you want to action on
741 e.g. C<--datafile /path/to/datafile>
745 Used to specify a path to find the local database schema and acls to be
748 e.g. C<--datadir /path/to/>
758 =item prompt-for-dba-password
760 Ask for the database administrator's password interactively
764 for 'init': skip creating the database and the user account, so we don't need
765 administrator privileges
767 =item root-password-file
769 for 'init' and 'insert': rather than using the default administrative password
770 for RT's "root" user, use the password in this file.
774 the name of the entity performing a create or upgrade. Used for logging changes
775 in the DB. Defaults to RT, otherwise it should be the fully qualified package name
776 of the extension or plugin making changes to the DB.
780 current version of extension making a change. Not needed for RT since RT has a
781 more elaborate system to track upgrades across multiple versions.
785 for 'upgrade': specifies the version to upgrade from, and do not prompt
786 for it if it appears to be a valid version.
790 for 'upgrade': specifies the version to upgrade to, and do not prompt
791 for it if it appears to be a valid version.