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