Upgrade 4.0.17 clean.
[usit-rt.git] / sbin / rt-setup-database
CommitLineData
84fb5b46
MKG
1#!/usr/bin/perl
2# BEGIN BPS TAGGED BLOCK {{{
3#
4# COPYRIGHT:
5#
403d7b0b 6# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
7# <sales@bestpractical.com>
8#
9# (Except where explicitly superseded by other copyright notices)
10#
11#
12# LICENSE:
13#
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
17# from www.gnu.org.
18#
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.
23#
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.
29#
30#
31# CONTRIBUTION SUBMISSION POLICY:
32#
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.)
38#
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.
47#
48# END BPS TAGGED BLOCK }}}
49use strict;
50use warnings;
51
52use vars qw($Nobody $SystemUser $item);
53
54# fix lib paths, some may be relative
55BEGIN {
56 require File::Spec;
57 my @libs = ("lib", "local/lib");
58 my $bin_path;
59
60 for my $lib (@libs) {
61 unless ( File::Spec->file_name_is_absolute($lib) ) {
62 unless ($bin_path) {
63 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
64 $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
65 }
66 else {
67 require FindBin;
68 no warnings "once";
69 $bin_path = $FindBin::Bin;
70 }
71 }
72 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
73 }
74 unshift @INC, $lib;
75 }
76
77}
78
79use Term::ReadKey;
80use Getopt::Long;
81
82$| = 1; # unbuffer all output.
83
84my %args = (
85 dba => 'postgres',
01e3b242 86 package => 'RT',
84fb5b46
MKG
87);
88GetOptions(
89 \%args,
90 'action=s',
91 'force', 'debug',
01e3b242 92 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
84fb5b46
MKG
93 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
94 'help|h',
95);
96
97no warnings 'once';
98if ( $args{help} || ! $args{'action'} ) {
99 require Pod::Usage;
100 Pod::Usage::pod2usage({ verbose => 2 });
101 exit;
102}
103
104require RT;
105RT->LoadConfig();
106RT->InitClasses();
107
108# Force warnings to be output to STDERR if we're not already logging
109# them at a higher level
110RT->Config->Set( LogToScreen => 'warning')
111 unless ( RT->Config->Get( 'LogToScreen' )
112 && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ );
113
114# get customized root password
115my $root_password;
116if ( $args{'root-password-file'} ) {
117 open( my $fh, '<', $args{'root-password-file'} )
118 or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
119 $root_password = <$fh>;
120 chomp $root_password;
121 my $min_length = RT->Config->Get('MinimumPasswordLength');
122 if ($min_length) {
123 die
124"password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
125 if length $root_password < $min_length;
126 }
127 close $fh;
128}
129
130
131# check and setup @actions
132my @actions = grep $_, split /,/, $args{'action'};
133if ( @actions > 1 && $args{'datafile'} ) {
134 print STDERR "You can not use --datafile option with multiple actions.\n";
135 exit(-1);
136}
137foreach ( @actions ) {
138 unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
139 print STDERR "$0 called with an invalid --action parameter.\n";
140 exit(-1);
141 }
142 if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
143 print STDERR "You can not mix init, drop or upgrade action with any action.\n";
144 exit(-1);
145 }
146}
147
148# convert init to multiple actions
149my $init = 0;
150if ( $actions[0] eq 'init' ) {
151 if ($args{'skip-create'}) {
152 @actions = qw(schema coredata insert);
153 } else {
154 @actions = qw(create schema acl coredata insert);
155 }
156 $init = 1;
157}
158
159# set options from environment
160foreach my $key(qw(Type Host Name User Password)) {
161 next unless exists $ENV{ 'RT_DB_'. uc $key };
162 print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
163 RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
164}
165
166my $db_type = RT->Config->Get('DatabaseType') || '';
167my $db_host = RT->Config->Get('DatabaseHost') || '';
01e3b242 168my $db_port = RT->Config->Get('DatabasePort') || '';
84fb5b46
MKG
169my $db_name = RT->Config->Get('DatabaseName') || '';
170my $db_user = RT->Config->Get('DatabaseUser') || '';
171my $db_pass = RT->Config->Get('DatabasePassword') || '';
172
173# load it here to get error immidiatly if DB type is not supported
174require RT::Handle;
175
176if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
177 $db_name = File::Spec->catfile($RT::VarPath, $db_name);
178 RT->Config->Set( DatabaseName => $db_name );
179}
180
181my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
182my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
183
184if ($args{'skip-create'}) {
185 $dba_user = $db_user;
186 $dba_pass = $db_pass;
187} else {
188 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
189 $dba_pass = get_dba_password();
190 chomp $dba_pass if defined($dba_pass);
191 }
192}
193
01e3b242
MKG
194my $version_word_regex = join '|', RT::Handle->version_words;
195my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
196
84fb5b46 197print "Working with:\n"
01e3b242 198 ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
84fb5b46
MKG
199 ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
200
201foreach my $action ( @actions ) {
202 no strict 'refs';
203 my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
204 error($action, $msg) unless $status;
205 print $msg .".\n" if $msg;
206 print "Done.\n";
207}
208
209sub action_create {
210 my %args = @_;
211 my $dbh = get_system_dbh();
5b0d0914 212 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
84fb5b46
MKG
213 return ($status, $msg) unless $status;
214
215 print "Now creating a $db_type database $db_name for RT.\n";
216 return RT::Handle->CreateDatabase( $dbh );
217}
218
219sub action_drop {
220 my %args = @_;
221
222 print "Dropping $db_type database $db_name.\n";
223 unless ( $args{'force'} ) {
224 print <<END;
225
01e3b242 226About to drop $db_type database $db_name on $db_host (port '$db_port').
84fb5b46
MKG
227WARNING: This will erase all data in $db_name.
228
229END
230 exit(-2) unless _yesno();
231 }
232
233 my $dbh = get_system_dbh();
234 return RT::Handle->DropDatabase( $dbh );
235}
236
237sub action_schema {
238 my %args = @_;
239 my $dbh = get_admin_dbh();
5b0d0914 240 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
84fb5b46
MKG
241 return ($status, $msg) unless $status;
242
243 print "Now populating database schema.\n";
244 return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
245}
246
247sub action_acl {
248 my %args = @_;
249 my $dbh = get_admin_dbh();
5b0d0914 250 my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
84fb5b46
MKG
251 return ($status, $msg) unless $status;
252
253 print "Now inserting database ACLs.\n";
254 return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
255}
256
257sub action_coredata {
258 my %args = @_;
259 $RT::Handle = RT::Handle->new;
260 $RT::Handle->dbh( undef );
261 RT::ConnectToDatabase();
262 RT::InitLogging();
5b0d0914 263 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
84fb5b46
MKG
264 return ($status, $msg) unless $status;
265
266 print "Now inserting RT core system objects.\n";
267 return $RT::Handle->InsertInitialData;
268}
269
270sub action_insert {
271 my %args = @_;
272 $RT::Handle = RT::Handle->new;
273 RT::Init();
5b0d0914 274 my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
84fb5b46
MKG
275 return ($status, $msg) unless $status;
276
277 print "Now inserting data.\n";
278 my $file = $args{'datafile'};
279 $file = $RT::EtcPath . "/initialdata" if $init && !$file;
280 $file ||= $args{'datadir'}."/content";
281
282 # Slurp in backcompat
283 my %removed;
284 my @back = @{$args{backcompat} || []};
285 if (@back) {
286 my @lines = do {local @ARGV = @back; <>};
287 for (@lines) {
288 s/\#.*//;
289 next unless /\S/;
290 my ($class, @fields) = split;
291 $class->_BuildTableAttributes;
292 $RT::Logger->debug("Temporarily removing @fields from $class");
293 $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
294 for @fields;
295 }
296 }
297
298 my @ret = $RT::Handle->InsertData( $file, $root_password );
299
300 # Put back the fields we chopped off
301 for my $class (keys %removed) {
302 $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
303 for keys %{$removed{$class}};
304 }
305 return @ret;
306}
307
308sub action_upgrade {
309 my %args = @_;
310 my $base_dir = $args{'datadir'} || "./etc/upgrade";
311 return (0, "Couldn't read dir '$base_dir' with upgrade data")
312 unless -d $base_dir || -r _;
313
314 my $upgrading_from = undef;
315 do {
316 if ( defined $upgrading_from ) {
317 print "Doesn't match #.#.#: ";
318 } else {
01e3b242 319 print "Enter $args{package} version you're upgrading from: ";
84fb5b46
MKG
320 }
321 $upgrading_from = scalar <STDIN>;
322 chomp $upgrading_from;
323 $upgrading_from =~ s/\s+//g;
01e3b242 324 } while $upgrading_from !~ /$version_dir/;
84fb5b46
MKG
325
326 my $upgrading_to = $RT::VERSION;
327 return (0, "The current version $upgrading_to is lower than $upgrading_from")
328 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
329
330 return (1, "The version $upgrading_to you're upgrading to is up to date")
331 if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
332
333 my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
334 return (1, "No DB changes since $upgrading_from")
335 unless @versions;
336
337 if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
338 print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
339 print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n";
340 $upgrading_to = $versions[-1];
341 }
342
343 print "\nGoing to apply following upgrades:\n";
344 print map "* $_\n", @versions;
345
346 {
347 my $custom_upgrading_to = undef;
348 do {
349 if ( defined $custom_upgrading_to ) {
350 print "Doesn't match #.#.#: ";
351 } else {
01e3b242 352 print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
84fb5b46
MKG
353 print " or leave it blank if you want apply above upgrades: ";
354 }
355 $custom_upgrading_to = scalar <STDIN>;
356 chomp $custom_upgrading_to;
357 $custom_upgrading_to =~ s/\s+//g;
358 last unless $custom_upgrading_to;
01e3b242 359 } while $custom_upgrading_to !~ /$version_dir/;
84fb5b46
MKG
360
361 if ( $custom_upgrading_to ) {
362 return (
363 0, "The version you entered ($custom_upgrading_to) is lower than\n"
364 ."version you're upgrading from ($upgrading_from)"
365 ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
366
367 return (1, "The version you're upgrading to is up to date")
368 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
369
370 if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
371 print "Version you entered is greater than installed ($RT::VERSION).\n";
372 _yesno() or exit(-2);
373 }
374 # ok, checked everything no let's refresh list
375 $upgrading_to = $custom_upgrading_to;
376 @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
377
378 return (1, "No DB changes between $upgrading_from and $upgrading_to")
379 unless @versions;
380
381 print "\nGoing to apply following upgrades:\n";
382 print map "* $_\n", @versions;
383 }
384 }
385
386 print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
387 _yesno() or exit(-2) unless $args{'force'};
388
389 my ( $ret, $msg );
390 foreach my $n ( 0..$#versions ) {
391 my $v = $versions[$n];
392 my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
393 print "Processing $v\n";
394 my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
395 if ( -e "$base_dir/$v/schema.$db_type" ) {
396 ( $ret, $msg ) = action_schema( %tmp );
397 return ( $ret, $msg ) unless $ret;
398 }
399 if ( -e "$base_dir/$v/acl.$db_type" ) {
400 ( $ret, $msg ) = action_acl( %tmp );
401 return ( $ret, $msg ) unless $ret;
402 }
403 if ( -e "$base_dir/$v/content" ) {
404 ( $ret, $msg ) = action_insert( %tmp );
405 return ( $ret, $msg ) unless $ret;
406 }
407 }
408 return 1;
409}
410
411sub get_versions_from_to {
412 my ($base_dir, $from, $to) = @_;
413
414 opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
01e3b242 415 my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
84fb5b46
MKG
416 closedir $dh;
417
01e3b242
MKG
418 die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
419 unless @versions;
420
84fb5b46
MKG
421 return
422 grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
423 grep RT::Handle::cmp_version($_, $from) > 0,
424 sort RT::Handle::cmp_version @versions;
425}
426
427sub error {
428 my ($action, $msg) = @_;
429 print STDERR "Couldn't finish '$action' step.\n\n";
430 print STDERR "ERROR: $msg\n\n";
431 exit(-1);
432}
433
434sub get_dba_password {
435 print "In order to create or update your RT database,"
436 . " this script needs to connect to your "
01e3b242 437 . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
84fb5b46
MKG
438 print "Please specify that user's database password below. If the user has no database\n";
439 print "password, just press return.\n\n";
440 print "Password: ";
441 ReadMode('noecho');
442 my $password = ReadLine(0);
443 ReadMode('normal');
444 print "\n";
445 return ($password);
446}
447
448# get_system_dbh
449# Returns L<DBI> database handle connected to B<system> with DBA credentials.
450# See also L<RT::Handle/SystemDSN>.
451
452
453sub get_system_dbh {
454 return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
455}
456
457sub get_admin_dbh {
458 return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
459}
460
461# get_rt_dbh [USER, PASSWORD]
462
463# Returns L<DBI> database handle connected to RT database,
464# you may specify credentials(USER and PASSWORD) to connect
465# with. By default connects with credentials from RT config.
466
467sub get_rt_dbh {
468 return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
469}
470
471sub _get_dbh {
472 my ($dsn, $user, $pass) = @_;
473 my $dbh = DBI->connect(
474 $dsn, $user, $pass,
475 { RaiseError => 0, PrintError => 0 },
476 );
477 unless ( $dbh ) {
478 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
479 if ( $args{'debug'} ) {
480 require Carp; Carp::confess( $msg );
481 } else {
482 print STDERR $msg; exit -1;
483 }
484 }
485 return $dbh;
486}
487
488sub _yesno {
489 print "Proceed [y/N]:";
490 my $x = scalar(<STDIN>);
491 $x =~ /^y/i;
492}
493
4941;
495
496__END__
497
498=head1 NAME
499
500rt-setup-database - Set up RT's database
501
502=head1 SYNOPSIS
503
504 rt-setup-database --action ...
505
506=head1 OPTIONS
507
508=over
509
510=item action
511
512Several actions can be combined using comma separated list.
513
514=over
515
516=item init
517
518Initialize the database. This is combination of multiple actions listed below.
519Create DB, schema, setup acl, insert core data and initial data.
520
521=item upgrade
522
523Apply all needed schema/acl/content updates (will ask for version to upgrade
524from)
525
526=item create
527
528Create the database.
529
530=item drop
531
532Drop the database. This will B<ERASE ALL YOUR DATA>.
533
534=item schema
535
536Initialize only the database schema
537
538To use a local or supplementary datafile, specify it using the '--datadir'
539option below.
540
541=item acl
542
543Initialize only the database ACLs
544
545To use a local or supplementary datafile, specify it using the '--datadir'
546option below.
547
548=item coredata
549
550Insert data into RT's database. This data is required for normal functioning of
551any RT instance.
552
553=item insert
554
555Insert data into RT's database. By default, will use RT's installation data.
556To use a local or supplementary datafile, specify it using the '--datafile'
557option below.
558
559=back
560
561=item datafile
562
563file path of the data you want to action on
564
565e.g. C<--datafile /path/to/datafile>
566
567=item datadir
568
569Used to specify a path to find the local database schema and acls to be
570installed.
571
572e.g. C<--datadir /path/to/>
573
574=item dba
575
576dba's username
577
578=item dba-password
579
580dba's password
581
582=item prompt-for-dba-password
583
584Ask for the database administrator's password interactively
585
586=item skip-create
587
588for 'init': skip creating the database and the user account, so we don't need
589administrator privileges
590
591=item root-password-file
592
593for 'init' and 'insert': rather than using the default administrative password
594for RT's "root" user, use the password in this file.
595
596=back