import rt 3.4.6
[freeside.git] / rt / sbin / rt-setup-database.in
1 #!@PERL@ -w
2 # BEGIN BPS TAGGED BLOCK {{{
3
4 # COPYRIGHT:
5 #  
6 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
27
28
29 # CONTRIBUTION SUBMISSION POLICY:
30
31 # (The following paragraph is not intended to limit the rights granted
32 # to you to modify and distribute this software under the terms of
33 # the GNU General Public License and is only of importance to you if
34 # you choose to contribute your changes and enhancements to the
35 # community by submitting them to Best Practical Solutions, LLC.)
36
37 # By intentionally submitting any modifications, corrections or
38 # derivatives to this work, or any other work intended for use with
39 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
40 # you are the copyright holder for those contributions and you grant
41 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
42 # royalty-free, perpetual, license to use, copy, create derivative
43 # works based on those contributions, and sublicense and distribute
44 # those contributions and any derivatives thereof.
45
46 # END BPS TAGGED BLOCK }}}
47 use strict;
48 use vars qw($PROMPT $VERSION $Handle $Nobody $SystemUser $item);
49 use vars
50   qw(@Groups @Users @ACL @Queues @ScripActions @ScripConditions @Templates @CustomFields @Scrips);
51
52 use lib "@RT_LIB_PATH@";
53
54 #This drags in  RT's config.pm
55 # We do it in a begin block because RT::Handle needs to know the type to do its
56 # inheritance
57 use RT;
58 use Carp;
59 use RT::User;
60 use RT::CurrentUser;
61 use RT::Template;
62 use RT::ScripAction;
63 use RT::ACE;
64 use RT::Group;
65 use RT::User;
66 use RT::Queue;
67 use RT::ScripCondition;
68 use RT::CustomField;
69 use RT::Scrip;
70
71 RT::LoadConfig();
72 use Term::ReadKey;
73 use Getopt::Long;
74
75 my %args;
76
77 GetOptions(
78     \%args,
79     'prompt-for-dba-password', 'force', 'debug',
80     'action=s',                'dba=s', 'dba-password=s', 'datafile=s',
81     'datadir=s'
82 );
83
84 $| = 1;    #unbuffer that output.
85
86 require RT::Handle;
87 my $Handle = RT::Handle->new($RT::DatabaseType);
88 $Handle->BuildDSN;
89 my $dbh;
90
91 if ( $args{'prompt-for-dba-password'} ) {
92     $args{'dba-password'} = get_dba_password();
93     chomp( $args{'dba-password'} );
94 }
95
96 unless ( $args{'action'} ) {
97     help();
98     die;
99 }
100 if ( $args{'action'} eq 'init' ) {
101     $dbh = DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} )
102       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
103     print "Now creating a database for RT.\n";
104     if ($RT::DatabaseType ne 'Oracle' ||
105         $args{'dba'} ne $RT::DatabaseUser) {
106     create_db();
107     } else {
108         print "...skipped as ".$args{'dba'} ." is not " . $RT::DatabaseUser . " or we're working with Oracle.\n";
109     }
110
111     if ($RT::DatabaseType eq "mysql") {
112         # Check which version we're running
113         my ($version) = $dbh->selectrow_hashref("show variables like 'version'")->{Value} =~ /^(\d\.\d+)/;
114         print "*** Warning: RT is unsupported on MySQL versions before 4.0.x\n" if $version < 4;
115
116         # MySQL must have InnoDB support
117         my $innodb = $dbh->selectrow_hashref("show variables like 'have_innodb'")->{Value};
118         if ($innodb eq "NO") {
119             die "RT requires that MySQL be compiled with InnoDB table support.\n".
120               "See http://dev.mysql.com/doc/mysql/en/InnoDB.html\n";
121         } elsif ($innodb eq "DISABLED") {
122             die "RT requires that MySQL InnoDB table support be enabled.\n".
123               ($version < 4
124                ? "Add 'innodb_data_file_path=ibdata1:10M:autoextend' to the [mysqld] section of my.cnf\n"
125                : "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
126         }
127     }
128     
129     # SQLite can't deal with the disconnect/reconnect
130     unless ($RT::DatabaseType eq 'SQLite') {
131
132         $dbh->disconnect;
133
134         if ($RT::DatabaseType eq "Oracle") {
135         $RT::DatabasePassword = $RT::DatabasePassword; #Warning avidance
136         $dbh = DBI->connect( $Handle->DSN, ${RT::DatabaseUser}, ${RT::DatabasePassword} ) || die $DBI::errstr;
137         } else {
138         
139         $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} ) || die $DBI::errstr;
140         }
141     }
142     print "Now populating database schema.\n";
143     insert_schema();
144     print "Now inserting database ACLs\n";
145     insert_acl() unless ($RT::DatabaseType eq 'Oracle');
146     print "Now inserting RT core system objects\n";
147     insert_initial_data();
148     print "Now inserting RT data\n";
149     insert_data( $RT::EtcPath . "/initialdata" );
150 }
151 elsif ( $args{'action'} eq 'drop' ) {
152     unless ( $dbh =
153          DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
154     {
155         warn $DBI::errstr;
156         warn "Database doesn't appear to exist. Aborting database drop.";
157         exit(0);
158     }
159     drop_db();
160 }
161 elsif ( $args{'action'} eq 'insert' ) {
162     insert_data( $args{'datafile'} || ($args{'datadir'}."/content"));
163 }
164 elsif ($args{'action'} eq 'acl') {
165     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
166       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
167      insert_acl($args{'datadir'});
168 }
169 elsif ($args{'action'} eq 'schema') {
170     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
171       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
172         insert_schema($args{'datadir'});
173 }
174
175 else {
176     print STDERR '$0 called with an invalid --action parameter';
177     exit(-1);
178 }
179
180 # {{{ sub insert_schema
181 sub insert_schema {
182         my $base_path = (shift || $RT::EtcPath);
183     my (@schema);
184     print "Creating database schema.\n";
185
186     if ( -f $base_path . "/schema." . $RT::DatabaseType ) {
187         no warnings 'unopened';
188
189         open( SCHEMA, "<" . $base_path . "/schema." . $RT::DatabaseType );
190         open( SCHEMA_LOCAL, "<" . $RT::LocalEtcPath . "/schema." . $RT::DatabaseType );
191
192         my $statement = "";
193         foreach my $line (<SCHEMA>, ($_ = ';;'), <SCHEMA_LOCAL>) {
194             $line =~ s/\#.*//g;
195             $line =~ s/--.*//g;
196             $statement .= $line;
197             if ( $line =~ /;(\s*)$/ ) {
198                 $statement =~ s/;(\s*)$//g;
199                 push @schema, $statement;
200                 $statement = "";
201             }
202         }
203
204         local $SIG{__WARN__} = sub {};
205         my $is_local = 0; # local/etc/schema needs to be nonfatal. 
206         $dbh->begin_work or die $dbh->errstr;
207         foreach my $statement (@schema) {
208             if ($statement =~ /^\s*;$/) { $is_local = 1; next; }
209             print STDERR "SQL: $statement\n" if defined $args{'debug'};
210             my $sth = $dbh->prepare($statement) or die $dbh->errstr;
211             unless ( $sth->execute or $is_local ) {
212                 die "Problem with statement:\n $statement\n" . $sth->errstr;
213             }
214         }
215         $dbh->commit or die $dbh->errstr;
216
217     }
218     else {
219         die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
220     }
221     print "Done setting up database schema.\n";
222
223 }
224
225 # }}}
226
227 # {{{ sub drop_db
228 sub drop_db {
229     if ( $RT::DatabaseType eq 'Oracle' ) {
230         print <<END;
231
232 To delete the tables and sequences of the RT Oracle database by running 
233     \@etc/drop.Oracle 
234 through SQLPlus.
235
236 END
237         return;
238     }   
239     unless ( $args{'force'} ) {
240         print <<END;
241
242 About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
243 WARNING: This will erase all data in $RT::DatabaseName.
244
245 END
246         exit unless _yesno();
247
248     }
249
250     print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";
251
252     if ( $RT::DatabaseType eq 'SQLite' ) {
253         unlink $RT::DatabaseName or warn $!;
254         return;
255     }
256     $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
257 }
258
259 # }}}
260
261 # {{{ sub create_db
262 sub create_db {
263     print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
264     if ( $RT::DatabaseType eq 'SQLite' ) {
265         return;
266     }
267     elsif ( $RT::DatabaseType eq 'Pg' ) {
268         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
269         if ($DBI::errstr) {
270             $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
271         }
272     }
273     elsif ($RT::DatabaseType eq 'Oracle') {
274         insert_acl();
275     }
276     elsif ( $RT::DatabaseType eq 'Informix' ) {
277         $ENV{DB_LOCALE} = 'en_us.utf8';
278         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
279     }
280     else {
281         $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
282     }
283 }
284
285 # }}}
286
287 sub get_dba_password {
288     print "In order to create or update your RT database,";
289     print "this script needs to connect to your "
290       . $RT::DatabaseType
291       . " instance on "
292       . $RT::DatabaseHost . " as "
293       . $args{'dba'} . ".\n";
294     print "Please specify that user's database password below. If the user has no database\n";
295     print "password, just press return.\n\n";
296     print "Password: ";
297     ReadMode('noecho');
298     my $password = ReadLine(0);
299     ReadMode('normal');
300     print "\n";
301     return ($password);
302 }
303
304 # {{{ sub _yesno
305 sub _yesno {
306     print "Proceed [y/N]:";
307     my $x = scalar(<STDIN>);
308     $x =~ /^y/i;
309 }
310
311 # }}}
312
313 # {{{ insert_acls
314 sub insert_acl {
315
316         my $base_path = (shift || $RT::EtcPath);
317
318     if ( $RT::DatabaseType =~ /^oracle$/i ) {
319         do $base_path . "/acl.Oracle"
320           || die "Couldn't find ACLS for Oracle\n" . $@;
321     }
322     elsif ( $RT::DatabaseType =~ /^pg$/i ) {
323         do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
324     }
325     elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
326         do $base_path . "/acl.mysql"
327           || die "Couldn't find ACLS for mysql in $base_path\n" . $@;
328     }
329     elsif ( $RT::DatabaseType =~ /^Sybase$/i ) {
330         do $base_path . "/acl.Sybase"
331           || die "Couldn't find ACLS for Sybase in $base_path\n" . $@;
332     }
333     elsif ( $RT::DatabaseType =~ /^informix$/i ) {
334         do $base_path . "/acl.Informix"
335           || die "Couldn't find ACLS for Informix in $base_path\n" . $@;
336     }
337     elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
338         return;
339     }
340     else {
341         die "Unknown RT database type";
342     }
343
344     my @acl = acl($dbh);
345     foreach my $statement (@acl) {
346         print STDERR $statement if $args{'debug'};
347         my $sth = $dbh->prepare($statement) or die $dbh->errstr;
348         unless ( $sth->execute ) {
349             die "Problem with statement:\n $statement\n" . $sth->errstr;
350         }
351     }
352     print "Done setting up database ACLs.\n";
353 }
354
355 # }}}
356
357 =head2 get_system_dsn
358
359 Returns a dsn suitable for database creates and drops
360 and user creates and drops
361
362 =cut
363
364 sub get_system_dsn {
365
366     my $dsn = $Handle->DSN;
367
368     #with mysql, you want to connect sans database to funge things
369     if ( $RT::DatabaseType eq 'mysql' ) {
370         $dsn =~ s/dbname=$RT::DatabaseName//;
371
372         # with postgres, you want to connect to database1
373     }
374     elsif ( $RT::DatabaseType eq 'Pg' ) {
375         $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
376     }
377     elsif ( $RT::DatabaseType eq 'Informix' ) {
378         # with Informix, you want to connect sans database:
379         $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
380     }
381     return $dsn;
382 }
383
384 sub insert_initial_data {
385
386     RT::InitLogging();
387
388     #connect to the db, for actual RT work
389     require RT::Handle;
390     $RT::Handle = RT::Handle->new();
391     $RT::Handle->Connect();
392
393     #Put together a current user object so we can create a User object
394     my $CurrentUser = new RT::CurrentUser();
395
396     print "Checking for existing system user...";
397     my $test_user = RT::User->new($CurrentUser);
398     $test_user->Load('RT_System');
399     if ( $test_user->id ) {
400         print "found!\n\nYou appear to have a functional RT database.\n"
401           . "Exiting, so as not to clobber your existing data.\n";
402         exit(-1);
403
404     }
405     else {
406         print "not found.  This appears to be a new installation.\n";
407     }
408
409     print "Creating system user...";
410     my $RT_System = new RT::User($CurrentUser);
411
412     my ( $val, $msg ) = $RT_System->_BootstrapCreate(
413         Name     => 'RT_System',
414         RealName => 'The RT System itself',
415         Comments =>
416 'Do not delete or modify this user. It is integral to RT\'s internal database structures',
417         Creator => '1',
418         LastUpdatedBy => '1' );
419
420     unless ($val) {
421         print "$msg\n";
422         exit(1);
423     }
424     print "done.\n";
425     $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
426
427 }
428
429 # load some sort of data into the database
430
431 sub insert_data {
432     my $datafile = shift;
433
434     #Connect to the database and get RT::SystemUser and RT::Nobody loaded
435     RT::Init;
436
437     my $CurrentUser = RT::CurrentUser->new();
438     $CurrentUser->LoadByName('RT_System');
439
440     if ( $datafile eq $RT::EtcPath . "/initialdata" ) {
441
442         print "Creating Superuser  ACL...";
443
444         my $superuser_ace = RT::ACE->new($CurrentUser);
445         $superuser_ace->_BootstrapCreate(
446                              PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
447                              PrincipalType => 'Group',
448                              RightName     => 'SuperUser',
449                              ObjectType    => 'RT::System',
450                              ObjectId      => '1' );
451
452         print "done.\n";
453     }
454
455     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
456     # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates
457
458     require $datafile
459       || die "Couldn't find initial data for import\n" . $@;
460
461     if (@Groups) {
462         print "Creating groups...";
463         foreach $item (@Groups) {
464             my $new_entry = RT::Group->new($CurrentUser);
465             my ( $return, $msg ) = $new_entry->_Create(%$item);
466             print "(Error: $msg)" unless ($return);
467             print $return. ".";
468         }
469         print "done.\n";
470     }
471     if (@Users) {
472         print "Creating users...";
473         foreach $item (@Users) {
474             my $new_entry = new RT::User($CurrentUser);
475             my ( $return, $msg ) = $new_entry->Create(%$item);
476             print "(Error: $msg)" unless ($return);
477             print $return. ".";
478         }
479         print "done.\n";
480     }
481     if (@Queues) {
482         print "Creating queues...";
483         for $item (@Queues) {
484             my $new_entry = new RT::Queue($CurrentUser);
485             my ( $return, $msg ) = $new_entry->Create(%$item);
486             print "(Error: $msg)" unless ($return);
487             print $return. ".";
488         }
489         print "done.\n";
490     }
491     if (@ACL) {
492         print "Creating ACL...";
493         for my $item (@ACL) {
494
495             my ($princ, $object);
496
497             # Global rights or Queue rights?
498             if ($item->{'Queue'}) {
499                 $object = RT::Queue->new($CurrentUser);
500                 $object->Load( $item->{'Queue'} );
501             } else {
502                 $object = $RT::System;
503             }
504
505             # Group rights or user rights?
506             if ($item->{'GroupDomain'}) {
507                 $princ = RT::Group->new($CurrentUser);
508                 if ($item->{'GroupDomain'} eq 'UserDefined') {
509                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
510                 } elsif ($item->{'GroupDomain'} eq 'SystemInternal') {
511                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
512                 } elsif ($item->{'GroupDomain'} eq 'RT::System-Role') {
513                   $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
514                 } elsif ($item->{'GroupDomain'} eq 'RT::Queue-Role' &&
515                          $item->{'Queue'}) {
516                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
517                                               Queue => $object->id);
518                 } else {
519                   $princ->Load( $item->{'GroupId'} );
520                 }
521             } else {
522                 $princ = RT::User->new($CurrentUser);
523                 $princ->Load( $item->{'UserId'} );
524             }
525
526             # Grant it
527             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
528                                                      Right => $item->{'Right'},
529                                                      Object => $object );
530
531             if ($return) {
532                 print $return. ".";
533             }
534             else {
535                 print $msg . ".";
536
537             }
538
539         }
540         print "done.\n";
541     }
542     if (@CustomFields) {
543         print "Creating custom fields...";
544         for $item (@CustomFields) {
545             my $new_entry = new RT::CustomField($CurrentUser);
546             my $values    = $item->{'Values'};
547             delete $item->{'Values'};
548             my $q     = $item->{'Queue'};
549             my $q_obj = RT::Queue->new($CurrentUser);
550             $q_obj->Load($q);
551             if ( $q_obj->Id ) {
552                 $item->{'Queue'} = $q_obj->Id;
553             }
554             elsif ( $q == 0 ) {
555                 $item->{'Queue'} = 0;
556             }
557             else {
558                 print "(Error: Could not find queue " . $q . ")\n"
559                   unless ( $q_obj->Id );
560                 next;
561             }
562             my ( $return, $msg ) = $new_entry->Create(%$item);
563             unless( $return ) {
564                 print "(Error: $msg)\n";
565                 next;
566             }
567
568             foreach my $value ( @{$values} ) {
569                 my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
570                 print "(Error: $emsg)\n" unless ($eval);
571             }
572
573             print "(Error: $msg)\n" unless ($return);
574             print $return. ".";
575         }
576
577         print "done.\n";
578     }
579
580     if (@ScripActions) {
581         print "Creating ScripActions...";
582
583         for $item (@ScripActions) {
584             my $new_entry = RT::ScripAction->new($CurrentUser);
585             my $return    = $new_entry->Create(%$item);
586             print $return. ".";
587         }
588
589         print "done.\n";
590     }
591
592     if (@ScripConditions) {
593         print "Creating ScripConditions...";
594
595         for $item (@ScripConditions) {
596             my $new_entry = RT::ScripCondition->new($CurrentUser);
597             my $return    = $new_entry->Create(%$item);
598             print $return. ".";
599         }
600
601         print "done.\n";
602     }
603
604     if (@Templates) {
605         print "Creating templates...";
606
607         for $item (@Templates) {
608             my $new_entry = new RT::Template($CurrentUser);
609             my $return    = $new_entry->Create(%$item);
610             print $return. ".";
611         }
612         print "done.\n";
613     }
614     if (@Scrips) {
615         print "Creating scrips...";
616
617         for $item (@Scrips) {
618             my $new_entry = new RT::Scrip($CurrentUser);
619             my ( $return, $msg ) = $new_entry->Create(%$item);
620             if ($return) {
621                 print $return. ".";
622             }
623             else {
624                 print "(Error: $msg)\n";
625             }
626         }
627         print "done.\n";
628     }
629     $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
630     print "Done setting up database content.\n";
631 }
632
633 =head2 ACLEquivGroupId
634
635 Given a userid, return that user's acl equivalence group
636
637 =cut
638
639 sub ACLEquivGroupId {
640     my $username = shift;
641     my $user     = RT::User->new($RT::SystemUser);
642     $user->Load($username);
643     my $equiv_group = RT::Group->new($RT::SystemUser);
644     $equiv_group->LoadACLEquivalenceGroup($user);
645     return ( $equiv_group->Id );
646 }
647
648 sub help {
649
650     print <<EOF;
651
652 $0: Set up RT's database
653
654 --action        init    Initialize the database
655                 drop    Drop the database. 
656                         This will ERASE ALL YOUR DATA
657                 insert  Insert data into RT's database. 
658                         By default, will use RT's installation data.
659                         To use a local or supplementary datafile, specify it
660                         using the '--datafile' option below.
661                         
662                 acl     Initialize only the database ACLs
663                         To use a local or supplementary datafile, specify it
664                         using the '--datadir' option below.
665                         
666                 schema  Initialize only the database schema
667                         To use a local or supplementary datafile, specify it
668                         using the '--datadir' option below.
669
670 --datafile /path/to/datafile
671 --datadir /path/to/              Used to specify a path to find the local
672                                 database schema and acls to be installed.
673
674
675 --dba                           dba's username
676 --dba-password                  dba's password
677 --prompt-for-dba-password       Ask for the database administrator's password interactively
678
679
680 EOF
681
682 }
683
684 1;