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