import rt 3.8.10
[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-2011 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 #This drags in  RT's config.pm
80 # We do it in a begin block because RT::Handle needs to know the type to do its
81 # inheritance
82 BEGIN {
83     use RT;
84     RT::LoadConfig();
85     RT::InitClasses();
86 }
87
88 use Term::ReadKey;
89 use Getopt::Long;
90
91 $| = 1; # unbuffer all output.
92
93 my %args;
94 GetOptions(
95     \%args,
96     'action=s',
97     'force', 'debug',
98     'dba=s', 'dba-password=s', 'prompt-for-dba-password',
99     'datafile=s', 'datadir=s'
100 );
101
102 unless ( $args{'action'} ) {
103     help();
104     exit(-1);
105 }
106
107 # check and setup @actions
108 my @actions = grep $_, split /,/, $args{'action'};
109 if ( @actions > 1 && $args{'datafile'} ) {
110     print STDERR "You can not use --datafile option with multiple actions.\n";
111     exit(-1);
112 }
113 foreach ( @actions ) {
114     unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
115         print STDERR "$0 called with an invalid --action parameter.\n";
116         exit(-1);
117     }
118     if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
119         print STDERR "You can not mix init, drop or upgrade action with any action.\n";
120         exit(-1);
121     }
122 }
123
124 # convert init to multiple actions
125 my $init = 0;
126 if ( $actions[0] eq 'init' ) {
127     @actions = qw(create schema acl coredata insert);
128     $init = 1;
129 }
130
131 # set options from environment
132 foreach my $key(qw(Type Host Name User Password)) {
133     next unless exists $ENV{ 'RT_DB_'. uc $key };
134     print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
135     RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
136 }
137
138 my $db_type = RT->Config->Get('DatabaseType') || '';
139 my $db_host = RT->Config->Get('DatabaseHost') || '';
140 my $db_name = RT->Config->Get('DatabaseName') || '';
141 my $db_user = RT->Config->Get('DatabaseUser') || '';
142 my $db_pass = RT->Config->Get('DatabasePassword') || '';
143
144 # load it here to get error immidiatly if DB type is not supported
145 require RT::Handle;
146
147 if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
148     $db_name = File::Spec->catfile($RT::VarPath, $db_name);
149     RT->Config->Set( DatabaseName => $db_name );
150 }
151
152 my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
153 my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};
154
155 if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
156     $dba_pass = get_dba_password();
157     chomp $dba_pass if defined($dba_pass);
158 }
159
160 print "Working with:\n"
161     ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
162     ."User:\t$db_user\nDBA:\t$dba_user\n";
163
164 foreach my $action ( @actions ) {
165     no strict 'refs';
166     my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
167     error($action, $msg) unless $status;
168     print $msg ."\n" if $msg;
169     print "Done.\n";
170 }
171
172 sub action_create {
173     my %args = @_;
174     my $dbh = get_system_dbh();
175     my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
176     return ($status, $msg) unless $status;
177
178     print "Now creating a $db_type database $db_name for RT.\n";
179     return RT::Handle->CreateDatabase( $dbh );
180 }
181
182 sub action_drop {
183     my %args = @_;
184
185     print "Dropping $db_type database $db_name.\n";
186     unless ( $args{'force'} ) {
187         print <<END;
188
189 About to drop $db_type database $db_name on $db_host.
190 WARNING: This will erase all data in $db_name.
191
192 END
193         exit(-2) unless _yesno();
194     }
195
196     my $dbh = get_system_dbh();
197     return RT::Handle->DropDatabase( $dbh );
198 }
199
200 sub action_schema {
201     my %args = @_;
202     my $dbh = get_admin_dbh();
203     my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
204     return ($status, $msg) unless $status;
205
206     print "Now populating database schema.\n";
207     return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
208 }
209
210 sub action_acl {
211     my %args = @_;
212     my $dbh = get_admin_dbh();
213     my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
214     return ($status, $msg) unless $status;
215
216     print "Now inserting database ACLs\n";
217     return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
218 }
219
220 sub action_coredata {
221     my %args = @_;
222     $RT::Handle = new RT::Handle;
223     $RT::Handle->dbh( undef );
224     RT::ConnectToDatabase();
225     RT::InitLogging();
226     my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
227     return ($status, $msg) unless $status;
228
229     print "Now inserting RT core system objects\n";
230     return $RT::Handle->InsertInitialData;
231 }
232
233 sub action_insert {
234     my %args = @_;
235     $RT::Handle = new RT::Handle;
236     RT::Init();
237     my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
238     return ($status, $msg) unless $status;
239
240     print "Now inserting data\n";
241     my $file = $args{'datafile'};
242     $file = $RT::EtcPath . "/initialdata" if $init && !$file;
243     $file ||= $args{'datadir'}."/content";
244     return $RT::Handle->InsertData( $file );
245 }
246
247 sub action_upgrade {
248     my %args = @_;
249     my $base_dir = $args{'datadir'} || "./etc/upgrade";
250     return (0, "Couldn't read dir '$base_dir' with upgrade data")
251         unless -d $base_dir || -r _;
252
253     my $upgrading_from = undef;
254     do {
255         if ( defined $upgrading_from ) {
256             print "Doesn't match #.#.#: ";
257         } else {
258             print "Enter RT version you're upgrading from: ";
259         }
260         $upgrading_from = scalar <STDIN>;
261         chomp $upgrading_from;
262         $upgrading_from =~ s/\s+//g;
263     } while $upgrading_from !~ /^\d+\.\d+\.\d+$/;
264
265     my $upgrading_to = $RT::VERSION;
266     return (0, "The current version $upgrading_to is lower than $upgrading_from")
267         if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
268
269     return (1, "The version $upgrading_to you're upgrading to is up to date")
270         if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
271
272     my @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
273
274     return (1, "No DB changes between $upgrading_from and $upgrading_to")
275         unless @versions;
276
277     print "\nGoing to apply following upgrades:\n";
278     print map "* $_\n", @versions;
279
280     {
281         my $custom_upgrading_to = undef;
282         do {
283             if ( defined $custom_upgrading_to ) {
284                 print "Doesn't match #.#.#: ";
285             } else {
286                 print "\nEnter RT version if you want to stop upgrade at some point,\n";
287                 print "  or leave it blank if you want apply above upgrades: ";
288             }
289             $custom_upgrading_to = scalar <STDIN>;
290             chomp $custom_upgrading_to;
291             $custom_upgrading_to =~ s/\s+//g;
292             last unless $custom_upgrading_to;
293         } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+$/;
294
295         if ( $custom_upgrading_to ) {
296             return (
297                 0, "The version you entered ($custom_upgrading_to) is lower than\n"
298                 ."version you're upgrading from ($upgrading_from)"
299             ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
300
301             return (1, "The version you're upgrading to is up to date")
302                 if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
303
304             if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
305                 print "Version you entered is greater than installed ($RT::VERSION).\n";
306                 _yesno() or exit(-2);
307             }
308             # ok, checked everything no let's refresh list
309             $upgrading_to = $custom_upgrading_to;
310             @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
311
312             return (1, "No DB changes between $upgrading_from and $upgrading_to")
313                 unless @versions;
314
315             print "\nGoing to apply following upgrades:\n";
316             print map "* $_\n", @versions;
317         }
318     }
319
320     print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
321     _yesno() or exit(-2) unless $args{'force'};
322
323     foreach my $v ( @versions ) {
324         print "Processing $v\n";
325         my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef);
326         if ( -e "$base_dir/$v/schema.$db_type" ) {
327             action_schema( %tmp );
328         }
329         if ( -e "$base_dir/$v/acl.$db_type" ) {
330             action_acl( %tmp );
331         }
332         if ( -e "$base_dir/$v/content" ) {
333             action_insert( %tmp );
334         }
335     }
336     return 1;
337 }
338
339 sub get_versions_from_to {
340     my ($base_dir, $from, $to) = @_;
341
342     opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
343     my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
344     closedir $dh;
345
346     return
347         grep RT::Handle::cmp_version($_, $to) <= 0,
348         grep RT::Handle::cmp_version($_, $from) > 0,
349         sort RT::Handle::cmp_version @versions;
350 }
351
352 sub error {
353     my ($action, $msg) = @_;
354     print STDERR "Couldn't finish '$action' step.\n\n";
355     print STDERR "ERROR: $msg\n\n";
356     exit(-1);
357 }
358
359 sub get_dba_password {
360     print "In order to create or update your RT database,"
361         . " this script needs to connect to your "
362         . " $db_type instance on $db_host as $dba_user\n";
363     print "Please specify that user's database password below. If the user has no database\n";
364     print "password, just press return.\n\n";
365     print "Password: ";
366     ReadMode('noecho');
367     my $password = ReadLine(0);
368     ReadMode('normal');
369     print "\n";
370     return ($password);
371 }
372
373 =head2 get_system_dbh
374
375 Returns L<DBI> database handle connected to B<system> with DBA credentials.
376
377 See also L<RT::Handle/SystemDSN>.
378
379 =cut
380
381 sub get_system_dbh {
382     return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
383 }
384
385 sub get_admin_dbh {
386     return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
387 }
388
389 =head2 get_rt_dbh [USER, PASSWORD]
390
391 Returns L<DBI> database handle connected to RT database,
392 you may specify credentials(USER and PASSWORD) to connect
393 with. By default connects with credentials from RT config.
394
395 =cut
396
397 sub get_rt_dbh {
398     return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
399 }
400
401 sub _get_dbh {
402     my ($dsn, $user, $pass) = @_;
403     my $dbh = DBI->connect(
404         $dsn, $user, $pass,
405         { RaiseError => 0, PrintError => 0 },
406     );
407     unless ( $dbh ) {
408         my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
409         if ( $args{'debug'} ) {
410             require Carp; Carp::confess( $msg );
411         } else {
412             print STDERR $msg; exit -1;
413         }
414     }
415     return $dbh;
416 }
417
418 sub _yesno {
419     print "Proceed [y/N]:";
420     my $x = scalar(<STDIN>);
421     $x =~ /^y/i;
422 }
423
424 sub help {
425
426     print <<EOF;
427
428 $0: Set up RT's database
429
430 --action        init     Initialize the database. This is combination of
431                          multiple actions listed below. Create DB, schema,
432                          setup acl, insert core data and initial data.
433
434                 upgrade  Apply all needed schema/acl/content updates (will ask
435                          for version to upgrade from)
436
437                 create   Create the database.
438
439                 drop     Drop the database.
440                          This will ERASE ALL YOUR DATA
441
442                 schema   Initialize only the database schema
443                          To use a local or supplementary datafile, specify it
444                          using the '--datadir' option below.
445
446                 acl      Initialize only the database ACLs
447                          To use a local or supplementary datafile, specify it
448                          using the '--datadir' option below.
449
450                 coredata Insert data into RT's database. This data is required
451                          for normal functioning of any RT instance.
452
453                 insert   Insert data into RT's database.
454                          By default, will use RT's installation data.
455                          To use a local or supplementary datafile, specify it
456                          using the '--datafile' option below.
457
458 Several actions can be combined using comma separated list.
459
460 --datafile /path/to/datafile
461 --datadir /path/to/              Used to specify a path to find the local
462                                  database schema and acls to be installed.
463
464
465 --dba                           dba's username
466 --dba-password                  dba's password
467 --prompt-for-dba-password       Ask for the database administrator's password interactively
468
469
470 EOF
471
472 }
473
474 1;