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