import rt 3.2.2
[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-2004 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         $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} ) || die $DBI::errstr;
134     }
135     print "Now populating database schema.\n";
136     insert_schema();
137     print "Now inserting database ACLs\n";
138     insert_acl() unless ($RT::DatabaseType eq 'Oracle');
139     print "Now inserting RT core system objects\n";
140     insert_initial_data();
141     print "Now inserting RT data\n";
142     insert_data( $RT::EtcPath . "/initialdata" );
143 }
144 elsif ( $args{'action'} eq 'drop' ) {
145     unless ( $dbh =
146          DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
147     {
148         warn $DBI::errstr;
149         warn "Database doesn't appear to exist. Aborting database drop.";
150         exit(0);
151     }
152     drop_db();
153 }
154 elsif ( $args{'action'} eq 'insert' ) {
155     insert_data( $args{'datafile'} || ($args{'datadir'}."/content"));
156 }
157 elsif ($args{'action'} eq 'acl') {
158     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
159       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
160      insert_acl($args{'datadir'});
161 }
162 elsif ($args{'action'} eq 'schema') {
163     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
164       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
165         insert_schema($args{'datadir'});
166 }
167
168 else {
169     print STDERR '$0 called with an invalid --action parameter';
170     exit(-1);
171 }
172
173 # {{{ sub insert_schema
174 sub insert_schema {
175         my $base_path = (shift || $RT::EtcPath);
176     my (@schema);
177     print "Creating database schema.\n";
178
179     if ( -f $base_path . "/schema." . $RT::DatabaseType ) {
180         no warnings 'unopened';
181
182         open( SCHEMA, "<" . $base_path . "/schema." . $RT::DatabaseType );
183         open( SCHEMA_LOCAL, "<" . $RT::LocalEtcPath . "/schema." . $RT::DatabaseType );
184
185         my $statement = "";
186         foreach my $line (<SCHEMA>, ($_ = ';;'), <SCHEMA_LOCAL>) {
187             $line =~ s/\#.*//g;
188             $line =~ s/--.*//g;
189             $statement .= $line;
190             if ( $line =~ /;(\s*)$/ ) {
191                 $statement =~ s/;(\s*)$//g;
192                 push @schema, $statement;
193                 $statement = "";
194             }
195         }
196
197         local $SIG{__WARN__} = sub {};
198         my $is_local = 0; # local/etc/schema needs to be nonfatal. 
199         foreach my $statement (@schema) {
200             if ($statement =~ /^\s*;$/) { $is_local = 1; next; }
201             print STDERR "SQL: $statement\n" if defined $args{'debug'};
202             my $sth = $dbh->prepare($statement) or die $dbh->errstr;
203             unless ( $sth->execute or $is_local ) {
204                 die "Problem with statement:\n $statement\n" . $sth->errstr;
205             }
206         }
207
208     }
209     else {
210         die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
211     }
212     print "schema sucessfully inserted\n";
213
214 }
215
216 # }}}
217
218 # {{{ sub drop_db
219 sub drop_db {
220     return if ( $RT::DatabaseType eq 'SQLite' );
221     if ( $RT::DatabaseType eq 'Oracle' ) {
222         print <<END;
223
224 To delete the tables and sequences of the RT Oracle database by running 
225     \@etc/drop.Oracle 
226 through SQLPlus.
227
228 END
229         return;
230     }   
231     unless ( $args{'force'} ) {
232         print <<END;
233
234 About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
235 WARNING: This will erase all data in $RT::DatabaseName.
236
237 END
238         exit unless _yesno();
239
240     }
241
242     print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";
243
244     $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
245 }
246
247 # }}}
248
249 # {{{ sub create_db
250 sub create_db {
251     print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
252     if ( $RT::DatabaseType eq 'SQLite' ) {
253         return;
254     }
255     elsif ( $RT::DatabaseType eq 'Pg' ) {
256         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
257         if ($DBI::errstr) {
258             $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
259         }
260     }
261     elsif ($RT::DatabaseType eq 'Oracle') {
262         insert_acl();
263     }
264     elsif ( $RT::DatabaseType eq 'Informix' ) {
265         $ENV{DB_LOCALE} = 'en_us.utf8';
266         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
267     }
268     else {
269         $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
270     }
271 }
272
273 # }}}
274
275 sub get_dba_password {
276     print
277 "In order to create a new database and grant RT access to that database,\n";
278     print "this script needs to connect to your "
279       . $RT::DatabaseType
280       . " instance on "
281       . $RT::DatabaseHost . " as "
282       . $args{'dba'} . ".\n";
283     print
284 "Please specify that user's database password below. If the user has no database\n";
285     print "password, just press return.\n\n";
286     print "Password: ";
287     ReadMode('noecho');
288     my $password = ReadLine(0);
289     ReadMode('normal');
290     return ($password);
291 }
292
293 # {{{ sub _yesno
294 sub _yesno {
295     print "Proceed [y/N]:";
296     my $x = scalar(<STDIN>);
297     $x =~ /^y/i;
298 }
299
300 # }}}
301
302 # {{{ insert_acls
303 sub insert_acl {
304
305         my $base_path = (shift || $RT::EtcPath);
306
307     if ( $RT::DatabaseType =~ /^oracle$/i ) {
308         do $base_path . "/acl.Oracle"
309           || die "Couldn't find ACLS for Oracle\n" . $@;
310     }
311     elsif ( $RT::DatabaseType =~ /^pg$/i ) {
312         do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
313     }
314     elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
315         do $base_path . "/acl.mysql"
316           || die "Couldn't find ACLS for mysql in " . $RT::EtcPath . "\n" . $@;
317     }
318     elsif ( $RT::DatabaseType =~ /^Sybase$/i ) {
319         do $base_path . "/acl.Sybase"
320           || die "Couldn't find ACLS for Sybase in " . $RT::EtcPath . "\n" . $@;
321     }
322     elsif ( $RT::DatabaseType =~ /^informix$/i ) {
323         do $base_path . "/acl.Informix"
324           || die "Couldn't find ACLS for Informix in " . $RT::EtcPath . "\n" . $@;
325     }
326     elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
327         return;
328     }
329     else {
330         die "Unknown RT database type";
331     }
332
333     my @acl = acl($dbh);
334     foreach my $statement (@acl) {
335         print STDERR $statement if $args{'debug'};
336         my $sth = $dbh->prepare($statement) or die $dbh->errstr;
337         unless ( $sth->execute ) {
338             die "Problem with statement:\n $statement\n" . $sth->errstr;
339         }
340     }
341 }
342
343 # }}}
344
345 =head2 get_system_dsn
346
347 Returns a dsn suitable for database creates and drops
348 and user creates and drops
349
350 =cut
351
352 sub get_system_dsn {
353
354     my $dsn = $Handle->DSN;
355
356     #with mysql, you want to connect sans database to funge things
357     if ( $RT::DatabaseType eq 'mysql' ) {
358         $dsn =~ s/dbname=$RT::DatabaseName//;
359
360         # with postgres, you want to connect to database1
361     }
362     elsif ( $RT::DatabaseType eq 'Pg' ) {
363         $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
364     }
365     elsif ( $RT::DatabaseType eq 'Informix' ) {
366         # with Informix, you want to connect sans database:
367         $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
368     }
369     return $dsn;
370 }
371
372 sub insert_initial_data {
373
374     RT::InitLogging();
375
376     #connect to the db, for actual RT work
377     require RT::Handle;
378     $RT::Handle = RT::Handle->new();
379     $RT::Handle->Connect();
380
381     #Put together a current user object so we can create a User object
382     my $CurrentUser = new RT::CurrentUser();
383
384     print "Checking for existing system user...";
385     my $test_user = RT::User->new($CurrentUser);
386     $test_user->Load('RT_System');
387     if ( $test_user->id ) {
388         print "found!\n\nYou appear to have a functional RT database.\n"
389           . "Exiting, so as not to clobber your existing data.\n";
390         exit(-1);
391
392     }
393     else {
394         print "not found.  This appears to be a new installation.\n";
395     }
396
397     print "Creating system user...";
398     my $RT_System = new RT::User($CurrentUser);
399
400     my ( $val, $msg ) = $RT_System->_BootstrapCreate(
401         Name     => 'RT_System',
402         RealName => 'The RT System itself',
403         Comments =>
404 'Do not delete or modify this user. It is integral to RT\'s internal database structures',
405         Creator => '1' );
406
407     unless ($val) {
408         print "$msg\n";
409         exit(1);
410     }
411     print "done.\n";
412     $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
413
414 }
415
416 # load some sort of data into the database
417
418 sub insert_data {
419     my $datafile = shift;
420
421     #Connect to the database and get RT::SystemUser and RT::Nobody loaded
422     RT::Init;
423
424     my $CurrentUser = RT::CurrentUser->new();
425     $CurrentUser->LoadByName('RT_System');
426
427     if ( $datafile eq $RT::EtcPath . "/initialdata" ) {
428
429         print "Creating Superuser  ACL...";
430
431         my $superuser_ace = RT::ACE->new($CurrentUser);
432         $superuser_ace->_BootstrapCreate(
433                              PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
434                              PrincipalType => 'Group',
435                              RightName     => 'SuperUser',
436                              ObjectType    => 'RT::System',
437                              ObjectId      => '1' );
438
439     }
440
441     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
442     # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates
443
444     require $datafile
445       || die "Couldn't find initial data for import\n" . $@;
446
447     if (@Groups) {
448         print "Creating groups...";
449         foreach $item (@Groups) {
450             my $new_entry = RT::Group->new($CurrentUser);
451             my ( $return, $msg ) = $new_entry->_Create(%$item);
452             print "(Error: $msg)" unless ($return);
453             print $return. ".";
454         }
455         print "done.\n";
456     }
457     if (@Users) {
458         print "Creating users...";
459         foreach $item (@Users) {
460             my $new_entry = new RT::User($CurrentUser);
461             my ( $return, $msg ) = $new_entry->Create(%$item);
462             print "(Error: $msg)" unless ($return);
463             print $return. ".";
464         }
465         print "done.\n";
466     }
467     if (@Queues) {
468         print "Creating queues...";
469         for $item (@Queues) {
470             my $new_entry = new RT::Queue($CurrentUser);
471             my ( $return, $msg ) = $new_entry->Create(%$item);
472             print "(Error: $msg)" unless ($return);
473             print $return. ".";
474         }
475         print "done.\n";
476     }
477     if (@ACL) {
478         print "Creating ACL...";
479         for my $item (@ACL) {
480
481             my ($princ, $object);
482
483             # Global rights or Queue rights?
484             if ($item->{'Queue'}) {
485                 $object = RT::Queue->new($CurrentUser);
486                 $object->Load( $item->{'Queue'} );
487             } else {
488                 $object = $RT::System;
489             }
490
491             # Group rights or user rights?
492             if ($item->{'GroupDomain'}) {
493                 $princ = RT::Group->new($CurrentUser);
494                 if ($item->{'GroupDomain'} eq 'UserDefined') {
495                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
496                 } elsif ($item->{'GroupDomain'} eq 'SystemInternal') {
497                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
498                 } elsif ($item->{'GroupDomain'} eq 'RT::System-Role') {
499                   $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
500                 } elsif ($item->{'GroupDomain'} eq 'RT::Queue-Role' &&
501                          $item->{'Queue'}) {
502                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
503                                               Queue => $object->id);
504                 } else {
505                   $princ->Load( $item->{'GroupId'} );
506                 }
507             } else {
508                 $princ = RT::User->new($CurrentUser);
509                 $princ->Load( $item->{'UserId'} );
510             }
511
512             # Grant it
513             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
514                                                      Right => $item->{'Right'},
515                                                      Object => $object );
516
517             if ($return) {
518                 print $return. ".";
519             }
520             else {
521                 print $msg . ".";
522
523             }
524
525         }
526         print "done.\n";
527     }
528     if (@CustomFields) {
529         print "Creating custom fields...";
530         for $item (@CustomFields) {
531             my $new_entry = new RT::CustomField($CurrentUser);
532             my $values    = $item->{'Values'};
533             delete $item->{'Values'};
534             my $q     = $item->{'Queue'};
535             my $q_obj = RT::Queue->new($CurrentUser);
536             $q_obj->Load($q);
537             if ( $q_obj->Id ) {
538                 $item->{'Queue'} = $q_obj->Id;
539             }
540             elsif ( $q == 0 ) {
541                 $item->{'Queue'} = 0;
542             }
543             else {
544                 print "(Error: Could not find queue " . $q . ")\n"
545                   unless ( $q_obj->Id );
546                 next;
547             }
548             my ( $return, $msg ) = $new_entry->Create(%$item);
549
550             foreach my $value ( @{$values} ) {
551                 my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
552                 print "(Error: $emsg)\n" unless ($eval);
553             }
554
555             print "(Error: $msg)\n" unless ($return);
556             print $return. ".";
557         }
558
559         print "done.\n";
560     }
561
562     if (@ScripActions) {
563         print "Creating ScripActions...";
564
565         for $item (@ScripActions) {
566             my $new_entry = RT::ScripAction->new($CurrentUser);
567             my $return    = $new_entry->Create(%$item);
568             print $return. ".";
569         }
570
571         print "done.\n";
572     }
573
574     if (@ScripConditions) {
575         print "Creating ScripConditions...";
576
577         for $item (@ScripConditions) {
578             my $new_entry = RT::ScripCondition->new($CurrentUser);
579             my $return    = $new_entry->Create(%$item);
580             print $return. ".";
581         }
582
583         print "done.\n";
584     }
585
586     if (@Templates) {
587         print "Creating templates...";
588
589         for $item (@Templates) {
590             my $new_entry = new RT::Template($CurrentUser);
591             my $return    = $new_entry->Create(%$item);
592             print $return. ".";
593         }
594         print "done.\n";
595     }
596     if (@Scrips) {
597         print "Creating scrips...";
598
599         for $item (@Scrips) {
600             my $new_entry = new RT::Scrip($CurrentUser);
601             my ( $return, $msg ) = $new_entry->Create(%$item);
602             if ($return) {
603                 print $return. ".";
604             }
605             else {
606                 print "(Error: $msg)\n";
607             }
608         }
609         print "done.\n";
610     }
611     $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
612
613 }
614
615 =head2 ACLEquivGroupId
616
617 Given a userid, return that user's acl equivalence group
618
619 =cut
620
621 sub ACLEquivGroupId {
622     my $username = shift;
623     my $user     = RT::User->new($RT::SystemUser);
624     $user->Load($username);
625     my $equiv_group = RT::Group->new($RT::SystemUser);
626     $equiv_group->LoadACLEquivalenceGroup($user);
627     return ( $equiv_group->Id );
628 }
629
630 sub help {
631
632     print <<EOF;
633
634 $0: Set up RT's database
635
636 --action        init    Initialize the database
637                 drop    Drop the database. 
638                         This will ERASE ALL YOUR DATA
639                 insert  Insert data into RT's database. 
640                         By default, will use RT's installation data.
641                         To use a local or supplementary datafile, specify it
642                         using the '--datafile' option below.
643                         
644                 acl     Initialize only the database ACLs
645                         To use a local or supplementary datafile, specify it
646                         using the '--datadir' option below.
647                         
648                 schema  Initialize only the database schema
649                         To use a local or supplementary datafile, specify it
650                         using the '--datadir' option below.
651
652 --datafile /path/to/datafile
653 --datadir /path/to/              Used to specify a path to find the local
654                                 database schema and acls to be installed.
655
656
657 --dba                           dba's username
658 --dba-password                  dba's password
659 --prompt-for-dba-password       Ask for the database administrator's password interactively
660
661
662 EOF
663
664 }
665
666 1;