Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / sbin / rt-setup-database.in
index 1257088..edf3d46 100644 (file)
@@ -3,7 +3,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -76,34 +76,59 @@ BEGIN {
 
 }
 
-#This drags in  RT's config.pm
-# We do it in a begin block because RT::Handle needs to know the type to do its
-# inheritance
-BEGIN {
-    use RT;
-    RT::LoadConfig();
-    RT::InitClasses();
-}
-
 use Term::ReadKey;
 use Getopt::Long;
 
 $| = 1; # unbuffer all output.
 
-my %args;
+my %args = (
+    dba => '@DB_DBA@',
+    package => 'RT',
+);
 GetOptions(
     \%args,
     'action=s',
     'force', 'debug',
-    'dba=s', 'dba-password=s', 'prompt-for-dba-password',
-    'datafile=s', 'datadir=s'
+    'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
+    'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
+    'upgrade-from=s', 'upgrade-to=s',
+    'help|h',
 );
 
-unless ( $args{'action'} ) {
-    help();
-    exit(-1);
+no warnings 'once';
+if ( $args{help} || ! $args{'action'} ) {
+    require Pod::Usage;
+    Pod::Usage::pod2usage({ verbose => 2 });
+    exit;
+}
+
+require RT;
+RT->LoadConfig();
+RT->InitClasses();
+
+# Force warnings to be output to STDERR if we're not already logging
+# them at a higher level
+RT->Config->Set( LogToScreen => 'warning')
+    unless ( RT->Config->Get( 'LogToScreen' )
+             && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ );
+
+# get customized root password
+my $root_password;
+if ( $args{'root-password-file'} ) {
+    open( my $fh, '<', $args{'root-password-file'} )
+      or die "Couldn't open 'args{'root-password-file'}' for reading: $!";
+    $root_password = <$fh>;
+    chomp $root_password;
+    my $min_length = RT->Config->Get('MinimumPasswordLength');
+    if ($min_length) {
+        die
+"password needs to be at least $min_length long, please check file '$args{'root-password-file'}'"
+          if length $root_password < $min_length;
+    }
+    close $fh;
 }
 
+
 # check and setup @actions
 my @actions = grep $_, split /,/, $args{'action'};
 if ( @actions > 1 && $args{'datafile'} ) {
@@ -124,7 +149,11 @@ foreach ( @actions ) {
 # convert init to multiple actions
 my $init = 0;
 if ( $actions[0] eq 'init' ) {
-    @actions = qw(create schema acl coredata insert);
+    if ($args{'skip-create'}) {
+        @actions = qw(schema coredata insert);
+    } else {
+        @actions = qw(create schema acl coredata insert);
+    }
     $init = 1;
 }
 
@@ -137,6 +166,7 @@ foreach my $key(qw(Type Host Name User Password)) {
 
 my $db_type = RT->Config->Get('DatabaseType') || '';
 my $db_host = RT->Config->Get('DatabaseHost') || '';
+my $db_port = RT->Config->Get('DatabasePort') || '';
 my $db_name = RT->Config->Get('DatabaseName') || '';
 my $db_user = RT->Config->Get('DatabaseUser') || '';
 my $db_pass = RT->Config->Get('DatabasePassword') || '';
@@ -154,27 +184,35 @@ my $dba_pass = exists($args{'dba-password'})
                  ? $args{'dba-password'}
                  : $ENV{'RT_DBA_PASSWORD'};
 
-if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
-    $dba_pass = get_dba_password();
-    chomp $dba_pass if defined($dba_pass);
+if ($args{'skip-create'}) {
+    $dba_user = $db_user;
+    $dba_pass = $db_pass;
+} else {
+    if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
+        $dba_pass = get_dba_password();
+        chomp $dba_pass if defined($dba_pass);
+    }
 }
 
+my $version_word_regex = join '|', RT::Handle->version_words;
+my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
+
 print "Working with:\n"
-    ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
-    ."User:\t$db_user\nDBA:\t$dba_user\n";
+    ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
+    ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
 
 foreach my $action ( @actions ) {
     no strict 'refs';
     my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
     error($action, $msg) unless $status;
-    print $msg ."\n" if $msg;
+    print $msg .".\n" if $msg;
     print "Done.\n";
 }
 
 sub action_create {
     my %args = @_;
     my $dbh = get_system_dbh();
-    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
     return ($status, $msg) unless $status;
 
     print "Now creating a $db_type database $db_name for RT.\n";
@@ -188,7 +226,7 @@ sub action_drop {
     unless ( $args{'force'} ) {
         print <<END;
 
-About to drop $db_type database $db_name on $db_host.
+About to drop $db_type database $db_name on $db_host (port '$db_port').
 WARNING: This will erase all data in $db_name.
 
 END
@@ -202,7 +240,7 @@ END
 sub action_schema {
     my %args = @_;
     my $dbh = get_admin_dbh();
-    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
     return ($status, $msg) unless $status;
 
     print "Now populating database schema.\n";
@@ -212,38 +250,62 @@ sub action_schema {
 sub action_acl {
     my %args = @_;
     my $dbh = get_admin_dbh();
-    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
     return ($status, $msg) unless $status;
 
-    print "Now inserting database ACLs\n";
+    print "Now inserting database ACLs.\n";
     return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
 }
 
 sub action_coredata {
     my %args = @_;
-    $RT::Handle = new RT::Handle;
+    $RT::Handle = RT::Handle->new;
     $RT::Handle->dbh( undef );
     RT::ConnectToDatabase();
     RT::InitLogging();
-    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
     return ($status, $msg) unless $status;
 
-    print "Now inserting RT core system objects\n";
+    print "Now inserting RT core system objects.\n";
     return $RT::Handle->InsertInitialData;
 }
 
 sub action_insert {
     my %args = @_;
-    $RT::Handle = new RT::Handle;
+    $RT::Handle = RT::Handle->new;
     RT::Init();
-    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
     return ($status, $msg) unless $status;
 
-    print "Now inserting data\n";
+    print "Now inserting data.\n";
     my $file = $args{'datafile'};
     $file = $RT::EtcPath . "/initialdata" if $init && !$file;
     $file ||= $args{'datadir'}."/content";
-    return $RT::Handle->InsertData( $file );
+
+    # Slurp in backcompat
+    my %removed;
+    my @back = @{$args{backcompat} || []};
+    if (@back) {
+        my @lines = do {local @ARGV = @back; <>};
+        for (@lines) {
+            s/\#.*//;
+            next unless /\S/;
+            my ($class, @fields) = split;
+            $class->_BuildTableAttributes;
+            $RT::Logger->debug("Temporarily removing @fields from $class");
+            $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_}
+                for @fields;
+        }
+    }
+
+    my @ret = $RT::Handle->InsertData( $file, $root_password );
+
+    # Put back the fields we chopped off
+    for my $class (keys %removed) {
+        $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_}
+            for keys %{$removed{$class}};
+    }
+    return @ret;
 }
 
 sub action_upgrade {
@@ -257,12 +319,12 @@ sub action_upgrade {
         if ( defined $upgrading_from ) {
             print "Doesn't match #.#.#: ";
         } else {
-            print "Enter RT version you're upgrading from: ";
+            print "Enter $args{package} version you're upgrading from: ";
         }
-        $upgrading_from = scalar <STDIN>;
+        $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>;
         chomp $upgrading_from;
         $upgrading_from =~ s/\s+//g;
-    } while $upgrading_from !~ /^\d+\.\d+\.\d+$/;
+    } while $upgrading_from !~ /$version_dir/;
 
     my $upgrading_to = $RT::VERSION;
     return (0, "The current version $upgrading_to is lower than $upgrading_from")
@@ -271,11 +333,16 @@ sub action_upgrade {
     return (1, "The version $upgrading_to you're upgrading to is up to date")
         if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;
 
-    my @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
-
-    return (1, "No DB changes between $upgrading_from and $upgrading_to")
+    my @versions = get_versions_from_to($base_dir, $upgrading_from, undef);
+    return (1, "No DB changes since $upgrading_from")
         unless @versions;
 
+    if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) {
+        print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n";
+        print   "***** which you are nominally upgrading to.  Upgrading to $versions[-1] instead.\n";
+        $upgrading_to = $versions[-1];
+    }
+
     print "\nGoing to apply following upgrades:\n";
     print map "* $_\n", @versions;
 
@@ -285,14 +352,14 @@ sub action_upgrade {
             if ( defined $custom_upgrading_to ) {
                 print "Doesn't match #.#.#: ";
             } else {
-                print "\nEnter RT version if you want to stop upgrade at some point,\n";
+                print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
                 print "  or leave it blank if you want apply above upgrades: ";
             }
-            $custom_upgrading_to = scalar <STDIN>;
+            $custom_upgrading_to = $args{'upgrade-to'} || scalar <STDIN>;
             chomp $custom_upgrading_to;
             $custom_upgrading_to =~ s/\s+//g;
             last unless $custom_upgrading_to;
-        } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+$/;
+        } while $custom_upgrading_to !~ /$version_dir/;
 
         if ( $custom_upgrading_to ) {
             return (
@@ -322,17 +389,23 @@ sub action_upgrade {
     print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
     _yesno() or exit(-2) unless $args{'force'};
 
-    foreach my $v ( @versions ) {
+    my ( $ret, $msg );
+    foreach my $n ( 0..$#versions ) {
+        my $v = $versions[$n];
+        my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
         print "Processing $v\n";
-        my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef);
+        my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
         if ( -e "$base_dir/$v/schema.$db_type" ) {
-            action_schema( %tmp );
+            ( $ret, $msg ) = action_schema( %tmp );
+            return ( $ret, $msg ) unless $ret;
         }
         if ( -e "$base_dir/$v/acl.$db_type" ) {
-            action_acl( %tmp );
+            ( $ret, $msg ) = action_acl( %tmp );
+            return ( $ret, $msg ) unless $ret;
         }
         if ( -e "$base_dir/$v/content" ) {
-            action_insert( %tmp );
+            ( $ret, $msg ) = action_insert( %tmp );
+            return ( $ret, $msg ) unless $ret;
         }
     }
     return 1;
@@ -342,11 +415,14 @@ sub get_versions_from_to {
     my ($base_dir, $from, $to) = @_;
 
     opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
-    my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
+    my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
     closedir $dh;
 
+    die "\nERROR: No upgrade data found in '$base_dir'!  Perhaps you specified the wrong --datadir?\n"
+        unless @versions;
+
     return
-        grep RT::Handle::cmp_version($_, $to) <= 0,
+        grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
         grep RT::Handle::cmp_version($_, $from) > 0,
         sort RT::Handle::cmp_version @versions;
 }
@@ -361,7 +437,7 @@ sub error {
 sub get_dba_password {
     print "In order to create or update your RT database,"
         . " this script needs to connect to your "
-        . " $db_type instance on $db_host as $dba_user\n";
+        . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
     print "Please specify that user's database password below. If the user has no database\n";
     print "password, just press return.\n\n";
     print "Password: ";
@@ -372,13 +448,10 @@ sub get_dba_password {
     return ($password);
 }
 
-=head2 get_system_dbh
-
-Returns L<DBI> database handle connected to B<system> with DBA credentials.
+#   get_system_dbh
+#   Returns L<DBI> database handle connected to B<system> with DBA credentials.
+#   See also L<RT::Handle/SystemDSN>.
 
-See also L<RT::Handle/SystemDSN>.
-
-=cut
 
 sub get_system_dbh {
     return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
@@ -388,13 +461,11 @@ sub get_admin_dbh {
     return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
 }
 
-=head2 get_rt_dbh [USER, PASSWORD]
-
-Returns L<DBI> database handle connected to RT database,
-you may specify credentials(USER and PASSWORD) to connect
-with. By default connects with credentials from RT config.
+# get_rt_dbh [USER, PASSWORD]
 
-=cut
+# Returns L<DBI> database handle connected to RT database,
+# you may specify credentials(USER and PASSWORD) to connect
+# with. By default connects with credentials from RT config.
 
 sub get_rt_dbh {
     return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
@@ -423,54 +494,116 @@ sub _yesno {
     $x =~ /^y/i;
 }
 
-sub help {
+1;
+
+__END__
 
-    print <<EOF;
+=head1 NAME
 
-$0: Set up RT's database
+rt-setup-database - Set up RT's database
 
---action        init     Initialize the database. This is combination of
-                         multiple actions listed below. Create DB, schema,
-                         setup acl, insert core data and initial data.
+=head1 SYNOPSIS
 
-                upgrade  Apply all needed schema/acl/content updates (will ask
-                         for version to upgrade from)
+    rt-setup-database --action ... 
 
-                create   Create the database.
+=head1 OPTIONS
 
-                drop     Drop the database.
-                         This will ERASE ALL YOUR DATA
+=over
 
-                schema   Initialize only the database schema
-                         To use a local or supplementary datafile, specify it
-                         using the '--datadir' option below.
+=item action
 
-                acl      Initialize only the database ACLs
-                         To use a local or supplementary datafile, specify it
-                         using the '--datadir' option below.
+Several actions can be combined using comma separated list.
 
-                coredata Insert data into RT's database. This data is required
-                         for normal functioning of any RT instance.
+=over
 
-                insert   Insert data into RT's database.
-                         By default, will use RT's installation data.
-                         To use a local or supplementary datafile, specify it
-                         using the '--datafile' option below.
+=item init
 
-Several actions can be combined using comma separated list.
+Initialize the database. This is combination of multiple actions listed below.
+Create DB, schema, setup acl, insert core data and initial data.
 
---datafile /path/to/datafile
---datadir /path/to/              Used to specify a path to find the local
-                                 database schema and acls to be installed.
+=item upgrade
 
+Apply all needed schema/acl/content updates (will ask for version to upgrade
+from)
 
---dba                           dba's username
---dba-password                  dba's password
---prompt-for-dba-password       Ask for the database administrator's password interactively
+=item create
 
+Create the database.
 
-EOF
+=item drop
 
-}
+Drop the database.  This will B<ERASE ALL YOUR DATA>.
 
-1;
+=item schema
+
+Initialize only the database schema
+
+To use a local or supplementary datafile, specify it using the '--datadir'
+option below.
+
+=item acl
+
+Initialize only the database ACLs
+
+To use a local or supplementary datafile, specify it using the '--datadir'
+option below.
+
+=item coredata 
+
+Insert data into RT's database. This data is required for normal functioning of
+any RT instance.
+
+=item insert
+
+Insert data into RT's database.  By default, will use RT's installation data.
+To use a local or supplementary datafile, specify it using the '--datafile'
+option below.
+
+=back
+
+=item datafile
+
+file path of the data you want to action on
+
+e.g. C<--datafile /path/to/datafile>
+
+=item datadir
+
+Used to specify a path to find the local database schema and acls to be
+installed.
+
+e.g. C<--datadir /path/to/>
+
+=item dba
+
+dba's username
+
+=item dba-password
+
+dba's password
+
+=item prompt-for-dba-password
+
+Ask for the database administrator's password interactively
+
+=item skip-create
+
+for 'init': skip creating the database and the user account, so we don't need
+administrator privileges
+
+=item root-password-file
+
+for 'init' and 'insert': rather than using the default administrative password
+for RT's "root" user, use the password in this file.
+
+=item upgrade-from
+
+for 'upgrade': specifies the version to upgrade from, and do not prompt
+for it if it appears to be a valid version.
+
+=item upgrade-to
+
+for 'upgrade': specifies the version to upgrade to, and do not prompt
+for it if it appears to be a valid version.
+
+=back