initial (hopefully rather unobtrusive) patch
[freeside.git] / rt / sbin / rt-setup-database
1 #!/usr/bin/perl -w
2 # BEGIN LICENSE BLOCK
3
4 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5
6 # (Except where explictly superceded by other copyright notices)
7
8 # This work is made available to you under the terms of Version 2 of
9 # the GNU General Public License. A copy of that license should have
10 # been provided with this software, but in any event can be snarfed
11 # from www.gnu.org.
12
13 # This work is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # General Public License for more details.
17
18 # Unless otherwise specified, all modifications, corrections or
19 # extensions to this work which alter its source code become the
20 # property of Best Practical Solutions, LLC when submitted for
21 # inclusion in the work.
22
23
24 # END LICENSE BLOCK
25
26 use strict;
27 use vars qw($PROMPT $VERSION $Handle $Nobody $SystemUser $item);
28 use vars
29   qw(@Groups @Users @ACL @Queues @ScripActions @ScripConditions @Templates @CustomFields @Scrips);
30
31 use lib "/opt/rt3/lib";
32
33 #This drags in  RT's config.pm
34 # We do it in a begin block because RT::Handle needs to know the type to do its
35 # inheritance
36 use RT;
37 use Carp;
38 use RT::User;
39 use RT::CurrentUser;
40 use RT::Template;
41 use RT::ScripAction;
42 use RT::ACE;
43 use RT::Group;
44 use RT::User;
45 use RT::Queue;
46 use RT::ScripCondition;
47 use RT::CustomField;
48 use RT::Scrip;
49
50 RT::LoadConfig();
51 use Term::ReadKey;
52 use Getopt::Long;
53
54 my %args;
55
56 GetOptions(
57     \%args,
58     'prompt-for-dba-password', 'force', 'debug',
59     'action=s',                'dba=s', 'dba-password=s', 'datafile=s',
60     'datadir=s'
61 );
62
63 $| = 1;    #unbuffer that output.
64
65 require RT::Handle;
66 my $Handle = RT::Handle->new($RT::DatabaseType);
67 $Handle->BuildDSN;
68 my $dbh;
69
70 if ( $args{'prompt-for-dba-password'} ) {
71     $args{'dba-password'} = get_dba_password();
72     chomp( $args{'dba-password'} );
73 }
74
75 unless ( $args{'action'} ) {
76     help();
77     die;
78 }
79 if ( $args{'action'} eq 'init' ) {
80     $dbh = DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} )
81       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
82     print "Now creating a database for RT.\n";
83     if ($RT::DatabaseType ne 'Oracle' ||
84         $args{'dba'} ne $RT::DatabaseUser) {
85     create_db();
86     } else {
87         print "...skipped as ".$args{'dba'} ." is not " . $RT::DatabaseUser . " or we're working with Oracle.\n";
88     }
89
90     $dbh->disconnect;
91     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
92       || die $DBI::errstr;
93
94     print "Now populating database schema.\n";
95     insert_schema();
96     print "Now inserting database ACLs\n";
97     insert_acl() unless ($RT::DatabaseType eq 'Oracle');
98     print "Now inserting RT core system objects\n";
99     insert_initial_data();
100     print "Now inserting RT data\n";
101     insert_data( $RT::EtcPath . "/initialdata" );
102 }
103 elsif ( $args{'action'} eq 'drop' ) {
104     unless ( $dbh =
105          DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
106     {
107         warn $DBI::errstr;
108         warn "Database doesn't appear to exist. Aborting database drop.";
109         exit(0);
110     }
111     drop_db();
112 }
113 elsif ( $args{'action'} eq 'insert_initial' ) {
114     insert_initial_data();
115 }
116 elsif ( $args{'action'} eq 'insert' ) {
117     insert_data( $args{'datafile'} );
118 }
119 elsif ($args{'action'} eq 'acl') {
120     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
121       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
122      insert_acl($args{'datadir'});
123 }
124 elsif ($args{'action'} eq 'schema') {
125     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
126       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
127         insert_schema($args{'datadir'});
128 }
129
130 else {
131     print STDERR '$0 called with an invalid --action parameter';
132     exit(-1);
133 }
134
135 # {{{ sub insert_schema
136 sub insert_schema {
137         my $base_path = (shift || $RT::EtcPath);
138     my (@schema);
139     print "Creating database schema.\n";
140
141     if ( -f $base_path . "/schema." . $RT::DatabaseType ) {
142         no warnings 'unopened';
143
144         open( SCHEMA, "<" . $base_path . "/schema." . $RT::DatabaseType );
145         open( SCHEMA_LOCAL, "<" . $RT::LocalEtcPath . "/schema." . $RT::DatabaseType );
146
147         my $statement = "";
148         foreach my $line (<SCHEMA>, ($_ = ';;'), <SCHEMA_LOCAL>) {
149             $line =~ s/\#.*//g;
150             $line =~ s/--.*//g;
151             $statement .= $line;
152             if ( $line =~ /;(\s*)$/ ) {
153                 $statement =~ s/;(\s*)$//g;
154                 push @schema, $statement;
155                 $statement = "";
156             }
157         }
158
159         local $SIG{__WARN__} = sub {};
160         my $is_local = 0; # local/etc/schema needs to be nonfatal. 
161         foreach my $statement (@schema) {
162             if ($statement =~ /^\s*;$/) { $is_local = 1; next; }
163             print STDERR "SQL: $statement\n" if defined $args{'debug'};
164             my $sth = $dbh->prepare($statement) or die $dbh->errstr;
165             unless ( $sth->execute or $is_local ) {
166                 die "Problem with statement:\n $statement\n" . $sth->errstr;
167             }
168         }
169
170     }
171     else {
172         die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
173     }
174     print "schema sucessfully inserted\n";
175
176 }
177
178 # }}}
179
180 # {{{ sub drop_db
181 sub drop_db {
182     return if ( $RT::DatabaseType eq 'SQLite' );
183     if ( $RT::DatabaseType eq 'Oracle' ) {
184         print <<END;
185
186 To delete the tables and sequences of the RT Oracle database by running 
187     \@etc/drop.Oracle 
188 through SQLPlus.
189
190 END
191         return;
192     }   
193     unless ( $args{'force'} ) {
194         print <<END;
195
196 About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
197 WARNING: This will erase all data in $RT::DatabaseName.
198
199 END
200         exit unless _yesno();
201
202     }
203
204     print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";
205
206     $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
207 }
208
209 # }}}
210
211 # {{{ sub create_db
212 sub create_db {
213     print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
214     if ( $RT::DatabaseType eq 'SQLite' ) {
215         return;
216     }
217     elsif ( $RT::DatabaseType eq 'Pg' ) {
218         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
219         if ($DBI::errstr) {
220             $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
221         }
222     }
223     elsif ($RT::DatabaseType eq 'Oracle') {
224         insert_acl();
225     }
226     elsif ( $RT::DatabaseType eq 'Informix' ) {
227         $ENV{DB_LOCALE} = 'en_us.utf8';
228         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
229     }
230     else {
231         $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
232     }
233 }
234
235 # }}}
236
237 sub get_dba_password {
238     print
239 "In order to create a new database and grant RT access to that database,\n";
240     print "this script needs to connect to your "
241       . $RT::DatabaseType
242       . " instance on "
243       . $RT::DatabaseHost . " as "
244       . $args{'dba'} . ".\n";
245     print
246 "Please specify that user's database password below. If the user has no database\n";
247     print "password, just press return.\n\n";
248     print "Password: ";
249     ReadMode('noecho');
250     my $password = ReadLine(0);
251     ReadMode('normal');
252     return ($password);
253 }
254
255 # {{{ sub _yesno
256 sub _yesno {
257     print "Proceed [y/N]:";
258     my $x = scalar(<STDIN>);
259     $x =~ /^y/i;
260 }
261
262 # }}}
263
264 # {{{ insert_acls
265 sub insert_acl {
266
267         my $base_path = (shift || $RT::EtcPath);
268
269     if ( $RT::DatabaseType =~ /^oracle$/i ) {
270         do $base_path . "/acl.Oracle"
271           || die "Couldn't find ACLS for Oracle\n" . $@;
272     }
273     elsif ( $RT::DatabaseType =~ /^pg$/i ) {
274         do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
275     }
276     elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
277         do $base_path . "/acl.mysql"
278           || die "Couldn't find ACLS for mysql in " . $RT::EtcPath . "\n" . $@;
279     }
280     elsif ( $RT::DatabaseType =~ /^informix$/i ) {
281         do $base_path . "/acl.Informix"
282           || die "Couldn't find ACLS for Informix in " . $RT::EtcPath . "\n" . $@;
283     }
284     elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
285         return;
286     }
287     else {
288         die "Unknown RT database type";
289     }
290
291     my @acl = acl($dbh);
292     foreach my $statement (@acl) {
293         print STDERR $statement if $args{'debug'};
294         my $sth = $dbh->prepare($statement) or die $dbh->errstr;
295         unless ( $sth->execute ) {
296             die "Problem with statement:\n $statement\n" . $sth->errstr;
297         }
298     }
299 }
300
301 # }}}
302
303 =head2 get_system_dsn
304
305 Returns a dsn suitable for database creates and drops
306 and user creates and drops
307
308 =cut
309
310 sub get_system_dsn {
311
312     my $dsn = $Handle->DSN;
313
314     #with mysql, you want to connect sans database to funge things
315     if ( $RT::DatabaseType eq 'mysql' ) {
316         $dsn =~ s/dbname=$RT::DatabaseName//;
317
318         # with postgres, you want to connect to database1
319     }
320     elsif ( $RT::DatabaseType eq 'Pg' ) {
321         $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
322     }
323     elsif ( $RT::DatabaseType eq 'Informix' ) {
324         # with Informix, you want to connect sans database:
325         $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
326     }
327     return $dsn;
328 }
329
330 sub insert_initial_data {
331
332     RT::InitLogging();
333
334     #connect to the db, for actual RT work
335     require RT::Handle;
336     $RT::Handle = RT::Handle->new();
337     $RT::Handle->Connect();
338
339     #Put together a current user object so we can create a User object
340     my $CurrentUser = new RT::CurrentUser();
341
342     print "Checking for existing system user ($CurrentUser)...";
343     my $test_user = RT::User->new($CurrentUser);
344     $test_user->Load('RT_System');
345     if ( $test_user->id ) {
346         print "found!\n\nYou appear to have a functional RT database.\n"
347           . "Exiting, so as not to clobber your existing data.\n";
348         exit(-1);
349
350     }
351     else {
352         print "not found.  This appears to be a new installation.\n";
353     }
354
355     print "Creating system user...";
356     my $RT_System = new RT::User($CurrentUser);
357
358     my ( $val, $msg ) = $RT_System->_BootstrapCreate(
359         Name     => 'RT_System',
360         RealName => 'The RT System itself',
361         Comments =>
362 'Do not delete or modify this user. It is integral to RT\'s internal database structures',
363         Creator => '1' );
364
365     unless ($val) {
366         print "$msg\n";
367         exit(1);
368     }
369     print "done.\n";
370     $RT::Handle->Disconnect();
371
372 }
373
374 # load some sort of data into the database
375
376 sub insert_data {
377     my $datafile = shift;
378
379     #Connect to the database and get RT::SystemUser and RT::Nobody loaded
380     RT::Init;
381
382     my $CurrentUser = RT::CurrentUser->new();
383     $CurrentUser->LoadByName('RT_System');
384
385     if ( $datafile eq $RT::EtcPath . "/initialdata" ) {
386
387         print "Creating Superuser  ACL...";
388
389         my $superuser_ace = RT::ACE->new($CurrentUser);
390         $superuser_ace->_BootstrapCreate(
391                              PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
392                              PrincipalType => 'Group',
393                              RightName     => 'SuperUser',
394                              ObjectType    => 'RT::System',
395                              ObjectId      => '1' );
396
397     }
398
399     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
400     # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates
401
402     require $datafile
403       || die "Couldn't find initial data for import\n" . $@;
404
405     if (@Groups) {
406         print "Creating groups...";
407         foreach $item (@Groups) {
408             my $new_entry = RT::Group->new($CurrentUser);
409             my ( $return, $msg ) = $new_entry->_Create(%$item);
410             print "(Error: $msg)" unless ($return);
411             print $return. ".";
412         }
413         print "done.\n";
414     }
415     if (@Users) {
416         print "Creating users...";
417         foreach $item (@Users) {
418             my $new_entry = new RT::User($CurrentUser);
419             my ( $return, $msg ) = $new_entry->Create(%$item);
420             print "(Error: $msg)" unless ($return);
421             print $return. ".";
422         }
423         print "done.\n";
424     }
425     if (@Queues) {
426         print "Creating queues...";
427         for $item (@Queues) {
428             my $new_entry = new RT::Queue($CurrentUser);
429             my ( $return, $msg ) = $new_entry->Create(%$item);
430             print "(Error: $msg)" unless ($return);
431             print $return. ".";
432         }
433         print "done.\n";
434     }
435     if (@ACL) {
436         print "Creating ACL...";
437         for my $item (@ACL) {
438
439             my ($princ, $object);
440
441             # Global rights or Queue rights?
442             if ($item->{'Queue'}) {
443                 $object = RT::Queue->new($CurrentUser);
444                 $object->Load( $item->{'Queue'} );
445             } else {
446                 $object = $RT::System;
447             }
448
449             # Group rights or user rights?
450             if ($item->{'GroupDomain'}) {
451                 $princ = RT::Group->new($CurrentUser);
452                 if ($item->{'GroupDomain'} eq 'UserDefined') {
453                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
454                 } elsif ($item->{'GroupDomain'} eq 'SystemInternal') {
455                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
456                 } elsif ($item->{'GroupDomain'} eq 'RT::Queue-Role' &&
457                          $item->{'Queue'}) {
458                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
459                                               Queue => $object->id);
460                 } else {
461                   $princ->Load( $item->{'GroupId'} );
462                 }
463             } else {
464                 $princ = RT::User->new($CurrentUser);
465                 $princ->Load( $item->{'UserId'} );
466             }
467
468             # Grant it
469             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
470                                                      Right => $item->{'Right'},
471                                                      Object => $object );
472
473             if ($return) {
474                 print $return. ".";
475             }
476             else {
477                 print $msg . ".";
478
479             }
480
481         }
482         print "done.\n";
483     }
484     if (@CustomFields) {
485         print "Creating custom fields...";
486         for $item (@CustomFields) {
487             my $new_entry = new RT::CustomField($CurrentUser);
488             my $values    = $item->{'Values'};
489             delete $item->{'Values'};
490             my $q     = $item->{'Queue'};
491             my $q_obj = RT::Queue->new($CurrentUser);
492             $q_obj->Load($q);
493             if ( $q_obj->Id ) {
494                 $item->{'Queue'} = $q_obj->Id;
495             }
496             elsif ( $q == 0 ) {
497                 $item->{'Queue'} = 0;
498             }
499             else {
500                 print "(Error: Could not find queue " . $q . ")\n"
501                   unless ( $q_obj->Id );
502                 next;
503             }
504             my ( $return, $msg ) = $new_entry->Create(%$item);
505
506             foreach my $value ( @{$values} ) {
507                 my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
508                 print "(Error: $emsg)\n" unless ($eval);
509             }
510
511             print "(Error: $msg)\n" unless ($return);
512             print $return. ".";
513         }
514
515         print "done.\n";
516     }
517
518     if (@ScripActions) {
519         print "Creating ScripActions...";
520
521         for $item (@ScripActions) {
522             my $new_entry = RT::ScripAction->new($CurrentUser);
523             my $return    = $new_entry->Create(%$item);
524             print $return. ".";
525         }
526
527         print "done.\n";
528     }
529
530     if (@ScripConditions) {
531         print "Creating ScripConditions...";
532
533         for $item (@ScripConditions) {
534             my $new_entry = RT::ScripCondition->new($CurrentUser);
535             my $return    = $new_entry->Create(%$item);
536             print $return. ".";
537         }
538
539         print "done.\n";
540     }
541
542     if (@Templates) {
543         print "Creating templates...";
544
545         for $item (@Templates) {
546             my $new_entry = new RT::Template($CurrentUser);
547             my $return    = $new_entry->Create(%$item);
548             print $return. ".";
549         }
550         print "done.\n";
551     }
552     if (@Scrips) {
553         print "Creating scrips...";
554
555         for $item (@Scrips) {
556             my $new_entry = new RT::Scrip($CurrentUser);
557             my ( $return, $msg ) = $new_entry->Create(%$item);
558             if ($return) {
559                 print $return. ".";
560             }
561             else {
562                 print "(Error: $msg)\n";
563             }
564         }
565         print "done.\n";
566     }
567     $RT::Handle->Disconnect();
568
569 }
570
571 =head2 ACLEquivGroupId
572
573 Given a userid, return that user's acl equivalence group
574
575 =cut
576
577 sub ACLEquivGroupId {
578     my $username = shift;
579     my $user     = RT::User->new($RT::SystemUser);
580     $user->Load($username);
581     my $equiv_group = RT::Group->new($RT::SystemUser);
582     $equiv_group->LoadACLEquivalenceGroup($user);
583     return ( $equiv_group->Id );
584 }
585
586 sub help {
587
588     print <<EOF;
589
590 $0: Set up RT's database
591
592 --action        init    Initialize the database
593                 drop    Drop the database. 
594                         This will ERASE ALL YOUR DATA
595                 insert  Insert data into RT's database. 
596                         By default, will use RT's installation data.
597                         To use a local or supplementary datafile, specify it
598                         using the '--datafile' option below.
599                         
600                 acl     Initialize only the database ACLs
601                         To use a local or supplementary datafile, specify it
602                         using the '--datadir' option below.
603                         
604                 schema  Initialize only the database schema
605                         To use a local or supplementary datafile, specify it
606                         using the '--datadir' option below.
607
608 --datafile /path/to/datafile
609 --datadir /path/to/              Used to specify a path to find the local
610                                 database schema and acls to be installed.
611
612
613 --dba                           dba's username
614 --dba-password                  dba's password
615 --prompt-for-dba-password       Ask for the database administrator's password interactively
616
617
618 EOF
619
620 }
621
622 1;