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