import rt 3.6.6
[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-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/copyleft/gpl.html.
29
30
31 # CONTRIBUTION SUBMISSION POLICY:
32
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47
48 # END BPS TAGGED BLOCK }}}
49 use strict;
50 use vars qw($PROMPT $VERSION $Handle $Nobody $SystemUser $item);
51 use vars
52   qw(@Groups @Users @ACL @Queues @ScripActions @ScripConditions @Templates @CustomFields @Scrips @Attributes);
53
54 use lib "@LOCAL_LIB_PATH@";
55 use lib "@RT_LIB_PATH@";
56
57 #This drags in  RT's config.pm
58 # We do it in a begin block because RT::Handle needs to know the type to do its
59 # inheritance
60 use RT;
61 use Carp;
62 use RT::User;
63 use RT::CurrentUser;
64 use RT::Template;
65 use RT::ScripAction;
66 use RT::ACE;
67 use RT::Group;
68 use RT::User;
69 use RT::Queue;
70 use RT::ScripCondition;
71 use RT::CustomField;
72 use RT::Scrip;
73
74 RT::LoadConfig();
75 use Term::ReadKey;
76 use Getopt::Long;
77
78 my %args;
79
80 GetOptions(
81     \%args,
82     'prompt-for-dba-password', 'force', 'debug',
83     'action=s',                'dba=s', 'dba-password=s', 'datafile=s',
84     'datadir=s'
85 );
86
87 unless ( $args{'action'} ) {
88     help();
89     exit(-1);
90 }
91
92 $| = 1;    #unbuffer that output.
93
94 require RT::Handle;
95 my $Handle = RT::Handle->new($RT::DatabaseType);
96 $Handle->BuildDSN;
97 my $dbh;
98
99 if ( $args{'prompt-for-dba-password'} ) {
100     $args{'dba-password'} = get_dba_password();
101     chomp( $args{'dba-password'} );
102 }
103
104 if ( $args{'action'} eq 'init' ) {
105     $dbh = DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} )
106       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
107     print "Now creating a database for RT.\n";
108     if ( $RT::DatabaseType ne 'Oracle' || $args{'dba'} ne $RT::DatabaseUser ) {
109         create_db();
110     } else {
111         print "...skipped as ".$args{'dba'} ." is not " . $RT::DatabaseUser . " or we're working with Oracle.\n";
112     }
113
114     if ( $RT::DatabaseType eq "mysql" ) {
115         # Check which version we're running
116         my ($version) = $dbh->selectrow_hashref("show variables like 'version'")->{Value} =~ /^(\d\.\d+)/;
117         print "*** Warning: RT is unsupported on MySQL versions before 4.0.x\n" if $version < 4;
118
119         # MySQL must have InnoDB support
120         my $innodb = $dbh->selectrow_hashref("show variables like 'have_innodb'")->{Value};
121         if ( $innodb eq "NO" ) {
122             die "RT requires that MySQL be compiled with InnoDB table support.\n".
123               "See http://dev.mysql.com/doc/mysql/en/InnoDB.html\n";
124         } elsif ( $innodb eq "DISABLED" ) {
125             die "RT requires that MySQL InnoDB table support be enabled.\n".
126               ($version < 4
127                ? "Add 'innodb_data_file_path=ibdata1:10M:autoextend' to the [mysqld] section of my.cnf\n"
128                : "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
129         }
130     }
131
132     # SQLite can't deal with the disconnect/reconnect
133     unless ( $RT::DatabaseType eq 'SQLite' ) {
134
135         $dbh->disconnect;
136
137         if ( $RT::DatabaseType eq "Oracle" ) {
138             $RT::DatabasePassword = $RT::DatabasePassword; #Warning avidance
139             $dbh = DBI->connect( $Handle->DSN, ${RT::DatabaseUser}, ${RT::DatabasePassword} ) || die $DBI::errstr;
140         } else {        
141             $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} ) || die $DBI::errstr;
142         }
143     }
144     print "Now populating database schema.\n";
145     insert_schema();
146     print "Now inserting database ACLs\n";
147     insert_acl() unless $RT::DatabaseType eq 'Oracle';
148     print "Now inserting RT core system objects\n";
149     insert_initial_data();
150     print "Now inserting RT data\n";
151     insert_data( $RT::EtcPath . "/initialdata" );
152 }
153 elsif ( $args{'action'} eq 'drop' ) {
154     unless ( $dbh =
155         DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
156     {
157         warn $DBI::errstr;
158         warn "Database doesn't appear to exist. Aborting database drop.";
159         exit;
160     }
161     drop_db();
162 }
163 elsif ( $args{'action'} eq 'insert' ) {
164     insert_data( $args{'datafile'} || ($args{'datadir'}."/content") );
165 }
166 elsif ( $args{'action'} eq 'acl' ) {
167     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
168       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
169     insert_acl($args{'datadir'});
170 }
171 elsif ( $args{'action'} eq 'schema' ) {
172     $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
173       || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
174     insert_schema($args{'datadir'});
175 }
176 else {
177     print STDERR "$0 called with an invalid --action parameter\n";
178     exit(-1);
179 }
180
181 # {{{ sub insert_schema
182 sub insert_schema {
183     my $base_path = (shift || $RT::EtcPath);
184     my (@schema);
185     print "Creating database schema.\n";
186
187     my $schema_file = $base_path . "/schema." . $RT::DatabaseType;
188     if ( -f $schema_file ) {
189         open( SCHEMA, "<$schema_file"  ) or die "Can't open $schema_file: $!";
190         my @lines = <SCHEMA>;
191
192         my $local_schema_file = $RT::LocalEtcPath . "/schema." . $RT::DatabaseType;
193         if (-f $local_schema_file) {
194             open( SCHEMA_LOCAL, "<$local_schema_file" )
195                 or die "Can't open $local_schema_file: $!";
196             push @lines, ';;', <SCHEMA_LOCAL>;
197         }
198
199         my $statement = "";
200         foreach my $line (@lines) {
201             $line =~ s/\#.*//g;
202             $line =~ s/--.*//g;
203             $statement .= $line;
204             if ( $line =~ /;(\s*)$/ ) {
205                 $statement =~ s/;(\s*)$//g;
206                 push @schema, $statement;
207                 $statement = "";
208             }
209         }
210
211         local $SIG{__WARN__} = sub {};
212         my $is_local = 0; # local/etc/schema needs to be nonfatal.
213         $dbh->begin_work or die $dbh->errstr;
214         foreach my $statement (@schema) {
215             if ( $statement =~ /^\s*;$/ ) { $is_local = 1; next; }
216
217             print STDERR "SQL: $statement\n" if defined $args{'debug'};
218             my $sth = $dbh->prepare($statement) or die $dbh->errstr;
219             unless ( $sth->execute or $is_local ) {
220                 die "Problem with statement:\n $statement\n" . $sth->errstr;
221             }
222         }
223         $dbh->commit or die $dbh->errstr;
224     }
225     else {
226         die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
227     }
228     print "Done setting up database schema.\n";
229 }
230
231 # }}}
232
233 # {{{ sub drop_db
234 sub drop_db {
235     if ( $RT::DatabaseType eq 'Oracle' ) {
236         print <<END;
237
238 To delete the tables and sequences of the RT Oracle database by running
239     \@etc/drop.Oracle
240 through SQLPlus.
241
242 END
243         return;
244     }
245     unless ( $args{'force'} ) {
246         print <<END;
247
248 About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
249 WARNING: This will erase all data in $RT::DatabaseName.
250
251 END
252         exit unless _yesno();
253
254     }
255
256     print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";
257
258     if ( $RT::DatabaseType eq 'SQLite' ) {
259         unlink $RT::DatabaseName or warn $!;
260         return;
261     }
262     $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
263 }
264
265 # }}}
266
267 # {{{ sub create_db
268 sub create_db {
269     print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
270     if ( $RT::DatabaseType eq 'SQLite' ) {
271         return;
272     }
273     elsif ( $RT::DatabaseType eq 'Pg' ) {
274         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
275         if ( $DBI::errstr ) {
276             $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
277         }
278     }
279     elsif ( $RT::DatabaseType eq 'Oracle' ) {
280         insert_acl();
281     }
282     elsif ( $RT::DatabaseType eq 'Informix' ) {
283         $ENV{DB_LOCALE} = 'en_us.utf8';
284         $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
285     }
286     else {
287         $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
288     }
289 }
290
291 # }}}
292
293 sub get_dba_password {
294     print "In order to create or update your RT database,";
295     print "this script needs to connect to your "
296       . $RT::DatabaseType
297       . " instance on "
298       . $RT::DatabaseHost . " as "
299       . $args{'dba'} . ".\n";
300     print "Please specify that user's database password below. If the user has no database\n";
301     print "password, just press return.\n\n";
302     print "Password: ";
303     ReadMode('noecho');
304     my $password = ReadLine(0);
305     ReadMode('normal');
306     print "\n";
307     return ($password);
308 }
309
310 # {{{ sub _yesno
311 sub _yesno {
312     print "Proceed [y/N]:";
313     my $x = scalar(<STDIN>);
314     $x =~ /^y/i;
315 }
316
317 # }}}
318
319 # {{{ insert_acls
320 sub insert_acl {
321     my $base_path = (shift || $RT::EtcPath);
322
323     if ( $RT::DatabaseType =~ /^oracle$/i ) {
324         do $base_path . "/acl.Oracle"
325           || die "Couldn't find ACLS for Oracle\n" . $@;
326     }
327     elsif ( $RT::DatabaseType =~ /^pg$/i ) {
328         do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
329     }
330     elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
331         do $base_path . "/acl.mysql"
332           || die "Couldn't find ACLS for mysql in $base_path\n" . $@;
333     }
334     elsif ( $RT::DatabaseType =~ /^Sybase$/i ) {
335         do $base_path . "/acl.Sybase"
336           || die "Couldn't find ACLS for Sybase in $base_path\n" . $@;
337     }
338     elsif ( $RT::DatabaseType =~ /^informix$/i ) {
339         do $base_path . "/acl.Informix"
340           || die "Couldn't find ACLS for Informix in $base_path\n" . $@;
341     }
342     elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
343         return;
344     }
345     else {
346         die "Unknown RT database type";
347     }
348
349     my @acl = acl($dbh);
350     foreach my $statement (@acl) {
351         print STDERR $statement if $args{'debug'};
352         my $sth = $dbh->prepare($statement) or die $dbh->errstr;
353         unless ( $sth->execute ) {
354             die "Problem with statement:\n $statement\n" . $sth->errstr;
355         }
356     }
357     print "Done setting up database ACLs.\n";
358 }
359
360 # }}}
361
362 =head2 get_system_dsn
363
364 Returns a dsn suitable for database creates and drops
365 and user creates and drops
366
367 =cut
368
369 sub get_system_dsn {
370
371     my $dsn = $Handle->DSN;
372
373     #with mysql, you want to connect sans database to funge things
374     if ( $RT::DatabaseType eq 'mysql' ) {
375         $dsn =~ s/dbname=$RT::DatabaseName//;
376
377         # with postgres, you want to connect to database1
378     }
379     elsif ( $RT::DatabaseType eq 'Pg' ) {
380         $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
381     }
382     elsif ( $RT::DatabaseType eq 'Informix' ) {
383         # with Informix, you want to connect sans database:
384         $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
385     }
386     return $dsn;
387 }
388
389 sub insert_initial_data {
390
391     RT::InitLogging();
392
393     #connect to the db, for actual RT work
394     require RT::Handle;
395     $RT::Handle = RT::Handle->new();
396     $RT::Handle->Connect();
397
398     #Put together a current user object so we can create a User object
399     my $CurrentUser = new RT::CurrentUser();
400
401     print "Checking for existing system user...";
402     my $test_user = RT::User->new($CurrentUser);
403     $test_user->Load('RT_System');
404     if ( $test_user->id ) {
405         print "found!\n\nYou appear to have a functional RT database.\n"
406           . "Exiting, so as not to clobber your existing data.\n";
407         exit(-1);
408
409     }
410     else {
411         print "not found.  This appears to be a new installation.\n";
412     }
413
414     print "Creating system user...";
415     my $RT_System = new RT::User($CurrentUser);
416
417     my ( $val, $msg ) = $RT_System->_BootstrapCreate(
418         Name     => 'RT_System',
419         RealName => 'The RT System itself',
420         Comments =>
421 'Do not delete or modify this user. It is integral to RT\'s internal database structures',
422         Creator => '1',
423         LastUpdatedBy => '1',
424     );
425
426     unless ( $val ) {
427         print "$msg\n";
428         exit(-1);
429     }
430     print "done.\n";
431     $RT::Handle->Disconnect() unless $RT::DatabaseType eq 'SQLite';
432
433 }
434
435 # load some sort of data into the database
436
437 sub insert_data {
438     my $datafile = shift;
439
440     #Connect to the database and get RT::SystemUser and RT::Nobody loaded
441     RT::Init;
442
443     my $CurrentUser = RT::CurrentUser->new();
444     $CurrentUser->LoadByName('RT_System');
445
446     if ( $datafile eq $RT::EtcPath . "/initialdata" ) {
447
448         print "Creating Superuser  ACL...";
449
450         my $superuser_ace = RT::ACE->new($CurrentUser);
451         $superuser_ace->_BootstrapCreate(
452                              PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
453                              PrincipalType => 'Group',
454                              RightName     => 'SuperUser',
455                              ObjectType    => 'RT::System',
456                              ObjectId      => '1' );
457
458         print "done.\n";
459     }
460
461     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
462     # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates
463
464     require $datafile
465       || die "Couldn't find initial data for import\n" . $@;
466
467     if ( @Groups ) {
468         print "Creating groups...";
469         foreach $item (@Groups) {
470             my $new_entry = RT::Group->new($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 ( @Users ) {
478         print "Creating users...";
479         foreach $item (@Users) {
480             my $new_entry = new RT::User($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 ( @Queues ) {
488         print "Creating queues...";
489         for $item (@Queues) {
490             my $new_entry = new RT::Queue($CurrentUser);
491             my ( $return, $msg ) = $new_entry->Create(%$item);
492             print "(Error: $msg)" unless $return;
493             print $return. ".";
494         }
495         print "done.\n";
496     }
497     if ( @ACL ) {
498         print "Creating ACL...";
499         for my $item (@ACL) {
500
501             my ($princ, $object);
502
503             # Global rights or Queue rights?
504             if ( $item->{'Queue'} ) {
505                 $object = RT::Queue->new($CurrentUser);
506                 $object->Load( $item->{'Queue'} );
507             } else {
508                 $object = $RT::System;
509             }
510
511             # Group rights or user rights?
512             if ( $item->{'GroupDomain'} ) {
513                 $princ = RT::Group->new($CurrentUser);
514                 if ( $item->{'GroupDomain'} eq 'UserDefined' ) {
515                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
516                 } elsif ( $item->{'GroupDomain'} eq 'SystemInternal' ) {
517                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
518                 } elsif ( $item->{'GroupDomain'} eq 'RT::System-Role' ) {
519                   $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
520                 } elsif ( $item->{'GroupDomain'} eq 'RT::Queue-Role' &&
521                           $item->{'Queue'} )
522                 {
523                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
524                                               Queue => $object->id);
525                 } else {
526                   $princ->Load( $item->{'GroupId'} );
527                 }
528             } else {
529                 $princ = RT::User->new($CurrentUser);
530                 $princ->Load( $item->{'UserId'} );
531             }
532
533             # Grant it
534             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
535                                                      Right => $item->{'Right'},
536                                                      Object => $object );
537
538             if ( $return ) {
539                 print $return. ".";
540             }
541             else {
542                 print $msg . ".";
543
544             }
545
546         }
547         print "done.\n";
548     }
549     if ( @CustomFields ) {
550         print "Creating custom fields...";
551         for $item (@CustomFields) {
552             my $new_entry = new RT::CustomField($CurrentUser);
553             my $values    = $item->{'Values'};
554             delete $item->{'Values'};
555             my ( $return, $msg ) = $new_entry->Create(%$item);
556             unless( $return ) {
557                 print "(Error: $msg)\n";
558                 next;
559             }
560
561             foreach my $value ( @{$values} ) {
562                 my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
563                 print "(Error: $emsg)\n" unless $eval;
564             }
565
566             if ( $item->{LookupType} && !exists $item->{'Queue'} ) { # enable by default
567                 my $ocf = RT::ObjectCustomField->new($CurrentUser);
568                 $ocf->Create( CustomField => $new_entry->Id );
569             }
570
571             print "(Error: $msg)\n" unless $return;
572             print $return. ".";
573         }
574
575         print "done.\n";
576     }
577
578     if ( @ScripActions ) {
579         print "Creating ScripActions...";
580
581         for $item (@ScripActions) {
582             my $new_entry = RT::ScripAction->new($CurrentUser);
583             my ($return,$msg) = $new_entry->Create(%$item);
584             unless ($return) {
585                 print "(Error: $msg)\n";
586                 next;
587             }
588             print $return. ".";
589         }
590
591         print "done.\n";
592     }
593
594     if ( @ScripConditions ) {
595         print "Creating ScripConditions...";
596
597         for $item (@ScripConditions) {
598             my $new_entry = RT::ScripCondition->new($CurrentUser);
599             my ($return,$msg) = $new_entry->Create(%$item);
600             unless ($return) {
601                 print "(Error: $msg)\n";
602                 next;
603             }
604             print $return. ".";
605         }
606
607         print "done.\n";
608     }
609
610     if ( @Templates ) {
611         print "Creating templates...";
612
613         for $item (@Templates) {
614             my $new_entry = new RT::Template($CurrentUser);
615             my $return    = $new_entry->Create(%$item);
616             print $return. ".";
617         }
618         print "done.\n";
619     }
620     if ( @Scrips ) {
621         print "Creating scrips...";
622
623         for $item (@Scrips) {
624             my $new_entry = new RT::Scrip($CurrentUser);
625             my ( $return, $msg ) = $new_entry->Create(%$item);
626             if ( $return ) {
627                 print $return. ".";
628             }
629             else {
630                 print "(Error: $msg)\n";
631             }
632         }
633         print "done.\n";
634     }
635     if ( @Attributes ) {
636         print "Creating predefined searches...";
637         my $sys = RT::System->new($CurrentUser);
638
639         for $item (@Attributes) {
640             my $obj = delete $item->{Object}; # XXX: make this something loadable
641             $obj ||= $sys;
642             my ( $return, $msg ) = $obj->AddAttribute (%$item);
643             if ( $return ) {
644                 print $return. ".";
645             }
646             else {
647                 print "(Error: $msg)\n";
648             }
649         }
650         print "done.\n";
651     }
652     $RT::Handle->Disconnect() unless $RT::DatabaseType eq 'SQLite';
653     print "Done setting up database content.\n";
654 }
655
656 =head2 ACLEquivGroupId
657
658 Given a userid, return that user's acl equivalence group
659
660 =cut
661
662 sub ACLEquivGroupId {
663     my $username = shift;
664     my $user     = RT::User->new($RT::SystemUser);
665     $user->Load($username);
666     my $equiv_group = RT::Group->new($RT::SystemUser);
667     $equiv_group->LoadACLEquivalenceGroup($user);
668     return ( $equiv_group->Id );
669 }
670
671 sub help {
672
673     print <<EOF;
674
675 $0: Set up RT's database
676
677 --action        init    Initialize the database
678                 drop    Drop the database.
679                         This will ERASE ALL YOUR DATA
680                 insert  Insert data into RT's database.
681                         By default, will use RT's installation data.
682                         To use a local or supplementary datafile, specify it
683                         using the '--datafile' option below.
684
685                 acl     Initialize only the database ACLs
686                         To use a local or supplementary datafile, specify it
687                         using the '--datadir' option below.
688
689                 schema  Initialize only the database schema
690                         To use a local or supplementary datafile, specify it
691                         using the '--datadir' option below.
692
693 --datafile /path/to/datafile
694 --datadir /path/to/              Used to specify a path to find the local
695                                 database schema and acls to be installed.
696
697
698 --dba                           dba's username
699 --dba-password                  dba's password
700 --prompt-for-dba-password       Ask for the database administrator's password interactively
701
702
703 EOF
704
705 }
706
707 1;