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