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