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 = ("lib", "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 = $args{'dba-password'} || $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 print "Working with:\n"
193 ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
194 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
196 foreach my $action ( @actions ) {
198 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
199 error($action, $msg) unless $status;
200 print $msg .".\n" if $msg;
206 my $dbh = get_system_dbh();
207 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
208 return ($status, $msg) unless $status;
210 print "Now creating a $db_type database $db_name for RT.\n";
211 return RT::Handle->CreateDatabase( $dbh );
217 print "Dropping $db_type database $db_name.\n";
218 unless ( $args{'force'} ) {
221 About to drop $db_type database $db_name on $db_host.
222 WARNING: This will erase all data in $db_name.
225 exit(-2) unless _yesno();
228 my $dbh = get_system_dbh();
229 return RT::Handle->DropDatabase( $dbh );
234 my $dbh = get_admin_dbh();
235 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
236 return ($status, $msg) unless $status;
238 print "Now populating database schema.\n";
239 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
244 my $dbh = get_admin_dbh();
245 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
246 return ($status, $msg) unless $status;
248 print "Now inserting database ACLs.\n";
249 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
252 sub action_coredata {
254 $RT::Handle = RT::Handle->new;
255 $RT::Handle->dbh( undef );
256 RT::ConnectToDatabase();
258 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
259 return ($status, $msg) unless $status;
261 print "Now inserting RT core system objects.\n";
262 return $RT::Handle->InsertInitialData;
267 $RT::Handle = RT::Handle->new;
269 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
270 return ($status, $msg) unless $status;
272 print "Now inserting data.\n";
273 my $file = $args{'datafile'};
274 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
275 $file ||= $args{'datadir'}."/content";
277 # Slurp in backcompat
279 my @back = @{$args{backcompat} || []};
281 my @lines = do {local @ARGV = @back; <>};
285 my ($class, @fields) = split;
286 $class->_BuildTableAttributes;
287 $RT::Logger->debug("Temporarily removing @fields from $class");
288 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
293 my @ret = $RT::Handle->InsertData( $file, $root_password );
295 # Put back the fields we chopped off
296 for my $class (keys %removed) {
297 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
298 for keys %{$removed{$class}};
305 my $base_dir = $args{'datadir'} || "./etc/upgrade";
306 return (0, "Couldn't read dir '$base_dir' with upgrade data")
307 unless -d $base_dir || -r _;
309 my $upgrading_from = undef;
311 if ( defined $upgrading_from ) {
312 print "Doesn't match #.#.#: ";
314 print "Enter RT version you're upgrading from: ";
316 $upgrading_from = scalar <STDIN>;
317 chomp $upgrading_from;
318 $upgrading_from =~ s/\s+//g;
319 } while $upgrading_from !~ /^\d+\.\d+\.\w+$/;
321 my $upgrading_to = $RT::VERSION;
322 return (0, "The current version $upgrading_to is lower than $upgrading_from")
323 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
325 return (1, "The version $upgrading_to you're upgrading to is up to date")
326 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
328 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
329 return (1, "No DB changes since $upgrading_from")
332 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
333 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
334 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
335 $upgrading_to = $versions[-1];
338 print "\nGoing to apply following upgrades:\n";
339 print map "* $_\n", @versions;
342 my $custom_upgrading_to = undef;
344 if ( defined $custom_upgrading_to ) {
345 print "Doesn't match #.#.#: ";
347 print "\nEnter RT version if you want to stop upgrade at some point,\n";
348 print " or leave it blank if you want apply above upgrades: ";
350 $custom_upgrading_to = scalar <STDIN>;
351 chomp $custom_upgrading_to;
352 $custom_upgrading_to =~ s/\s+//g;
353 last unless $custom_upgrading_to;
354 } while $custom_upgrading_to !~ /^\d+\.\d+\.\w+$/;
356 if ( $custom_upgrading_to ) {
358 0, "The version you entered ($custom_upgrading_to) is lower than\n"
359 ."version you're upgrading from ($upgrading_from)"
360 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
362 return (1, "The version you're upgrading to is up to date")
363 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
365 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
366 print "Version you entered is greater than installed ($RT::VERSION).\n";
367 _yesno() or exit(-2);
369 # ok, checked everything no let's refresh list
370 $upgrading_to = $custom_upgrading_to;
371 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
373 return (1, "No DB changes between $upgrading_from and $upgrading_to")
376 print "\nGoing to apply following upgrades:\n";
377 print map "* $_\n", @versions;
381 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
382 _yesno() or exit(-2) unless $args{'force'};
385 foreach my $n ( 0..$#versions ) {
386 my $v = $versions[$n];
387 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
388 print "Processing $v\n";
389 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
390 if ( -e "$base_dir/$v/schema.$db_type" ) {
391 ( $ret, $msg ) = action_schema( %tmp );
392 return ( $ret, $msg ) unless $ret;
394 if ( -e "$base_dir/$v/acl.$db_type" ) {
395 ( $ret, $msg ) = action_acl( %tmp );
396 return ( $ret, $msg ) unless $ret;
398 if ( -e "$base_dir/$v/content" ) {
399 ( $ret, $msg ) = action_insert( %tmp );
400 return ( $ret, $msg ) unless $ret;
406 sub get_versions_from_to {
407 my ($base_dir, $from, $to) = @_;
409 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
410 my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
414 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
415 grep RT::Handle::cmp_version($_, $from) > 0,
416 sort RT::Handle::cmp_version @versions;
420 my ($action, $msg) = @_;
421 print STDERR "Couldn't finish '$action' step.\n\n";
422 print STDERR "ERROR: $msg\n\n";
426 sub get_dba_password {
427 print "In order to create or update your RT database,"
428 . " this script needs to connect to your "
429 . " $db_type instance on $db_host as $dba_user\n";
430 print "Please specify that user's database password below. If the user has no database\n";
431 print "password, just press return.\n\n";
434 my $password = ReadLine(0);
441 # Returns L<DBI> database handle connected to B<system> with DBA credentials.
442 # See also L<RT::Handle/SystemDSN>.
446 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
450 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
453 # get_rt_dbh [USER, PASSWORD]
455 # Returns L<DBI> database handle connected to RT database,
456 # you may specify credentials(USER and PASSWORD) to connect
457 # with. By default connects with credentials from RT config.
460 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
464 my ($dsn, $user, $pass) = @_;
465 my $dbh = DBI->connect(
467 { RaiseError => 0, PrintError => 0 },
470 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
471 if ( $args{'debug'} ) {
472 require Carp; Carp::confess( $msg );
474 print STDERR $msg; exit -1;
481 print "Proceed [y/N]:";
482 my $x = scalar(<STDIN>);
492 rt-setup-database - Set up RT's database
496 rt-setup-database --action ...
504 Several actions can be combined using comma separated list.
510 Initialize the database. This is combination of multiple actions listed below.
511 Create DB, schema, setup acl, insert core data and initial data.
515 Apply all needed schema/acl/content updates (will ask for version to upgrade
524 Drop the database. This will B<ERASE ALL YOUR DATA>.
528 Initialize only the database schema
530 To use a local or supplementary datafile, specify it using the '--datadir'
535 Initialize only the database ACLs
537 To use a local or supplementary datafile, specify it using the '--datadir'
542 Insert data into RT's database. This data is required for normal functioning of
547 Insert data into RT's database. By default, will use RT's installation data.
548 To use a local or supplementary datafile, specify it using the '--datafile'
555 file path of the data you want to action on
557 e.g. C<--datafile /path/to/datafile>
561 Used to specify a path to find the local database schema and acls to be
564 e.g. C<--datadir /path/to/>
574 =item prompt-for-dba-password
576 Ask for the database administrator's password interactively
580 for 'init': skip creating the database and the user account, so we don't need
581 administrator privileges
583 =item root-password-file
585 for 'init' and 'insert': rather than using the default administrative password
586 for RT's "root" user, use the password in this file.