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
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',
91 if ( $args{help} || ! $args{'action'} ) {
93 Pod::Usage::pod2usage({ verbose => 2 });
101 # Force warnings to be output to STDERR if we're not already logging
102 # them at a higher level
103 RT->Config->Set( LogToSTDERR => 'warning')
104 unless ( RT->Config->Get( 'LogToSTDERR' )
105 && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ );
108 # get customized root password
110 if ( $args{'root-password-file'} ) {
111 open( my $fh, '<', $args{'root-password-file'} )
112 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
113 $root_password = <$fh>;
114 chomp $root_password;
115 my $min_length = RT->Config->Get('MinimumPasswordLength');
118 "password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
119 if length $root_password < $min_length;
125 # check and setup @actions
126 my @actions = grep $_, split /,/, $args{'action'};
127 if ( @actions > 1 && $args{'datafile'} ) {
128 print STDERR "You can not use --datafile option with multiple actions.\n";
131 foreach ( @actions ) {
132 unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) {
133 print STDERR "$0 called with an invalid --action parameter.\n";
136 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
137 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
142 # convert init to multiple actions
144 if ( $actions[0] eq 'init' ) {
145 if ($args{'skip-create'}) {
146 @actions = qw(schema coredata insert);
148 @actions = qw(create schema acl coredata insert);
153 # set options from environment
154 foreach my $key(qw(Type Host Name User Password)) {
155 next unless exists $ENV{ 'RT_DB_'. uc $key };
156 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
157 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
160 my $db_type = RT->Config->Get('DatabaseType') || '';
161 my $db_host = RT->Config->Get('DatabaseHost') || '';
162 my $db_port = RT->Config->Get('DatabasePort') || '';
163 my $db_name = RT->Config->Get('DatabaseName') || '';
164 my $db_user = RT->Config->Get('DatabaseUser') || '';
165 my $db_pass = RT->Config->Get('DatabasePassword') || '';
167 # load it here to get error immidiatly if DB type is not supported
170 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
171 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
172 RT->Config->Set( DatabaseName => $db_name );
175 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || RT->Config->Get('DatabaseAdmin') || '';
176 my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
178 if ($args{'skip-create'}) {
179 $dba_user = $db_user;
180 $dba_pass = $db_pass;
182 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
183 $dba_pass = get_dba_password();
184 chomp $dba_pass if defined($dba_pass);
188 my $version_word_regex = join '|', RT::Handle->version_words;
189 my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
191 print "Working with:\n"
192 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
193 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
195 my $package = $args{'package'} || 'RT';
196 my $ext_version = $args{'ext-version'};
197 my $full_id = Data::GUID->new->as_string;
200 if ($args{'package'} ne 'RT') {
201 RT->ConnectToDatabase();
202 RT->InitSystemObjects();
206 foreach my $action ( @actions ) {
208 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
209 error($action, $msg) unless $status;
210 print $msg .".\n" if $msg;
216 my $dbh = get_system_dbh();
217 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
218 return ($status, $msg) unless $status;
220 print "Now creating a $db_type database $db_name for RT.\n";
221 return RT::Handle->CreateDatabase( $dbh );
227 print "Dropping $db_type database $db_name.\n";
228 unless ( $args{'force'} ) {
231 About to drop $db_type database $db_name on $db_host (port '$db_port').
232 WARNING: This will erase all data in $db_name.
235 exit(-2) unless _yesno();
238 my $dbh = get_system_dbh();
239 return RT::Handle->DropDatabase( $dbh );
244 my $dbh = get_admin_dbh();
245 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
246 return ($status, $msg) unless $status;
248 my $individual_id = Data::GUID->new->as_string();
251 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
254 individual_id => $individual_id,
256 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
257 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
259 print "Now populating database schema.\n";
260 my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
264 individual_id => $individual_id,
265 return_value => [ @ret ],
267 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
274 my $dbh = get_admin_dbh();
275 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
276 return ($status, $msg) unless $status;
278 my $individual_id = Data::GUID->new->as_string();
281 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
284 individual_id => $individual_id,
286 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
287 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
289 print "Now inserting database ACLs.\n";
290 my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
294 individual_id => $individual_id,
295 return_value => [ @ret ],
297 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
304 RT->ConnectToDatabase;
305 my $individual_id = Data::GUID->new->as_string();
308 filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
311 individual_id => $individual_id,
313 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
314 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
316 my $dbh = get_admin_dbh();
317 $RT::Handle = RT::Handle->new;
318 $RT::Handle->dbh( $dbh );
321 print "Now inserting database indexes.\n";
322 my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} );
324 $RT::Handle = RT::Handle->new;
325 $RT::Handle->dbh( undef );
326 RT->ConnectToDatabase;
329 individual_id => $individual_id,
330 return_value => [ @ret ],
332 RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
337 sub action_coredata {
339 $RT::Handle = RT::Handle->new;
340 $RT::Handle->dbh( undef );
341 RT::ConnectToDatabase();
342 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
343 return ($status, $msg) unless $status;
345 print "Now inserting RT core system objects.\n";
346 return $RT::Handle->InsertInitialData;
351 $RT::Handle = RT::Handle->new;
355 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
356 return ($status, $msg) unless $status;
358 print "Now inserting data.\n";
359 my $file = $args{'datafile'};
360 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
361 $file ||= $args{'datadir'}."/content";
363 my $individual_id = Data::GUID->new->as_string();
366 filename => Cwd::abs_path($file),
369 individual_id => $individual_id
371 $upgrade_data{'ext_version'} = $ext_version if $ext_version;
373 open my $handle, '<', $file or warn "Unable to open $file: $!";
374 $upgrade_data{content} = do {local $/; <$handle>} if $handle;
376 RT->System->AddUpgradeHistory($package => \%upgrade_data);
380 my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) };
382 for my $file (@{$args{backcompat} || []}) {
383 my $lines = do {local $/; local @ARGV = ($file); <>};
384 my $sub = eval "sub {\n# line 1 $file\n$lines\n}";
386 warn "Failed to load backcompat $file: $@";
389 my $current = $upgrade;
390 $upgrade = sub { $sub->($current) };
395 # XXX Reconnecting to insert the history entry
396 # until we can sort out removing
397 # the disconnect at the end of InsertData.
398 RT->ConnectToDatabase();
402 individual_id => $individual_id,
403 return_value => [ @ret ],
406 RT->System->AddUpgradeHistory($package => \%upgrade_data);
408 my $db_type = RT->Config->Get('DatabaseType');
409 $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
416 my $base_dir = $args{'datadir'} || "./etc/upgrade";
417 return (0, "Couldn't read dir '$base_dir' with upgrade data")
418 unless -d $base_dir || -r _;
420 my $upgrading_from = undef;
422 if ( defined $upgrading_from ) {
423 print "Doesn't match #.#.#: ";
425 print "Enter $args{package} version you're upgrading from: ";
427 $upgrading_from = scalar <STDIN>;
428 chomp $upgrading_from;
429 $upgrading_from =~ s/\s+//g;
430 } while $upgrading_from !~ /$version_dir/;
432 my $upgrading_to = $RT::VERSION;
433 return (0, "The current version $upgrading_to is lower than $upgrading_from")
434 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
436 return (1, "The version $upgrading_to you're upgrading to is up to date")
437 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
439 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
440 return (1, "No DB changes since $upgrading_from")
443 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
444 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
445 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
446 $upgrading_to = $versions[-1];
449 print "\nGoing to apply following upgrades:\n";
450 print map "* $_\n", @versions;
453 my $custom_upgrading_to = undef;
455 if ( defined $custom_upgrading_to ) {
456 print "Doesn't match #.#.#: ";
458 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
459 print " or leave it blank if you want apply above upgrades: ";
461 $custom_upgrading_to = scalar <STDIN>;
462 chomp $custom_upgrading_to;
463 $custom_upgrading_to =~ s/\s+//g;
464 last unless $custom_upgrading_to;
465 } while $custom_upgrading_to !~ /$version_dir/;
467 if ( $custom_upgrading_to ) {
469 0, "The version you entered ($custom_upgrading_to) is lower than\n"
470 ."version you're upgrading from ($upgrading_from)"
471 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
473 return (1, "The version you're upgrading to is up to date")
474 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
476 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
477 print "Version you entered is greater than installed ($RT::VERSION).\n";
478 _yesno() or exit(-2);
480 # ok, checked everything no let's refresh list
481 $upgrading_to = $custom_upgrading_to;
482 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
484 return (1, "No DB changes between $upgrading_from and $upgrading_to")
487 print "\nGoing to apply following upgrades:\n";
488 print map "* $_\n", @versions;
492 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
493 _yesno() or exit(-2) unless $args{'force'};
495 RT->ConnectToDatabase();
496 RT->InitSystemObjects();
499 RT->System->AddUpgradeHistory($package => {
500 type => 'full upgrade',
503 from => $upgrading_from,
505 versions => [@versions],
507 individual_id => $full_id
510 # Ensure that the Attributes column is big enough to hold the
511 # upgrade steps we're going to add; this step exists in 4.0.6 for
512 # mysql, but that may be too late. Run it as soon as possible.
513 if (RT->Config->Get('DatabaseType') eq 'mysql'
514 and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) {
515 my $dbh = get_admin_dbh();
516 # Before the binary switch in 3.7.87, we want to alter text ->
517 # longtext, not blob -> longblob
518 if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) {
519 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT")
521 $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB")
525 my $previous = $upgrading_from;
527 foreach my $n ( 0..$#versions ) {
528 my $v = $versions[$n];
529 my $individual_id = Data::GUID->new->as_string();
531 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
532 print "Processing $v\n";
534 RT->System->AddUpgradeHistory($package => {
536 type => 'individual upgrade',
541 individual_id => $individual_id,
544 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
546 if ( -e "$base_dir/$v/schema.$db_type" ) {
547 ( $ret, $msg ) = action_schema( %tmp );
548 return ( $ret, $msg ) unless $ret;
550 if ( -e "$base_dir/$v/acl.$db_type" ) {
551 ( $ret, $msg ) = action_acl( %tmp );
552 return ( $ret, $msg ) unless $ret;
554 if ( -e "$base_dir/$v/indexes" ) {
555 ( $ret, $msg ) = action_indexes( %tmp );
556 return ( $ret, $msg ) unless $ret;
558 if ( -e "$base_dir/$v/content" ) {
559 ( $ret, $msg ) = action_insert( %tmp );
560 return ( $ret, $msg ) unless $ret;
563 # XXX: Another connect since the insert called
564 # previous to this step will disconnect.
566 RT->ConnectToDatabase();
568 RT->System->AddUpgradeHistory($package => {
570 individual_id => $individual_id,
576 RT->System->AddUpgradeHistory($package => {
578 individual_id => $full_id,
584 sub get_versions_from_to {
585 my ($base_dir, $from, $to) = @_;
587 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
588 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
591 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
595 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
596 grep RT::Handle::cmp_version($_, $from) > 0,
597 sort RT::Handle::cmp_version @versions;
601 my ($action, $msg) = @_;
602 print STDERR "Couldn't finish '$action' step.\n\n";
603 print STDERR "ERROR: $msg\n\n";
607 sub get_dba_password {
608 print "In order to create or update your RT database,"
609 . " this script needs to connect to your "
610 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
611 print "Please specify that user's database password below. If the user has no database\n";
612 print "password, just press return.\n\n";
615 my $password = ReadLine(0);
622 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
623 # See also L<RT::Handle/SystemDSN>.
627 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
631 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
634 # get_rt_dbh [USER, PASSWORD]
636 # Returns L<DBI> database handle connected to RT database,
637 # you may specify credentials(USER and PASSWORD) to connect
638 # with. By default connects with credentials from RT config.
641 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
645 my ($dsn, $user, $pass) = @_;
646 my $dbh = DBI->connect(
648 { RaiseError => 0, PrintError => 0 },
651 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
652 if ( $args{'debug'} ) {
653 require Carp; Carp::confess( $msg );
655 print STDERR $msg; exit -1;
662 print "Proceed [y/N]:";
663 my $x = scalar(<STDIN>);
673 rt-setup-database - Set up RT's database
677 rt-setup-database --action ...
685 Several actions can be combined using comma separated list.
691 Initialize the database. This is combination of multiple actions listed below.
692 Create DB, schema, setup acl, insert core data and initial data.
696 Apply all needed schema/acl/content updates (will ask for version to upgrade
705 Drop the database. This will B<ERASE ALL YOUR DATA>.
709 Initialize only the database schema
711 To use a local or supplementary datafile, specify it using the '--datadir'
716 Initialize only the database ACLs
718 To use a local or supplementary datafile, specify it using the '--datadir'
723 Insert data into RT's database. This data is required for normal functioning of
728 Insert data into RT's database. By default, will use RT's installation data.
729 To use a local or supplementary datafile, specify it using the '--datafile'
736 file path of the data you want to action on
738 e.g. C<--datafile /path/to/datafile>
742 Used to specify a path to find the local database schema and acls to be
745 e.g. C<--datadir /path/to/>
755 =item prompt-for-dba-password
757 Ask for the database administrator's password interactively
761 for 'init': skip creating the database and the user account, so we don't need
762 administrator privileges
764 =item root-password-file
766 for 'init' and 'insert': rather than using the default administrative password
767 for RT's "root" user, use the password in this file.
771 the name of the entity performing a create or upgrade. Used for logging changes
772 in the DB. Defaults to RT, otherwise it should be the fully qualified package name
773 of the extension or plugin making changes to the DB.
777 current version of extension making a change. Not needed for RT since RT has a
778 more elaborate system to track upgrades across multiple versions.