default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / rt / sbin / rt-setup-database.in
index 06c04dc..11e2358 100644 (file)
@@ -1,39 +1,41 @@
-#!@PERL@ -w
+#!@PERL@
 # BEGIN BPS TAGGED BLOCK {{{
-# 
+#
 # COPYRIGHT:
-#  
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
-#                                          <jesse@bestpractical.com>
-# 
+#
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
 # (Except where explicitly superseded by other copyright notices)
-# 
-# 
+#
+#
 # LICENSE:
-# 
+#
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-# 
+#
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-# 
-# 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
 # CONTRIBUTION SUBMISSION POLICY:
-# 
+#
 # (The following paragraph is not intended to limit the rights granted
 # to you to modify and distribute this software under the terms of
 # the GNU General Public License and is only of importance to you if
 # you choose to contribute your changes and enhancements to the
 # community by submitting them to Best Practical Solutions, LLC.)
-# 
+#
 # By intentionally submitting any modifications, corrections or
 # derivatives to this work, or any other work intended for use with
 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
 # royalty-free, perpetual, license to use, copy, create derivative
 # works based on those contributions, and sublicense and distribute
 # those contributions and any derivatives thereof.
-# 
+#
 # END BPS TAGGED BLOCK }}}
 use strict;
-use vars qw($PROMPT $VERSION $Handle $Nobody $SystemUser $item);
-use vars
-  qw(@Groups @Users @ACL @Queues @ScripActions @ScripConditions @Templates @CustomFields @Scrips);
-
-use lib "@RT_LIB_PATH@";
-
-#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
-use RT;
-use Carp;
-use RT::User;
-use RT::CurrentUser;
-use RT::Template;
-use RT::ScripAction;
-use RT::ACE;
-use RT::Group;
-use RT::User;
-use RT::Queue;
-use RT::ScripCondition;
-use RT::CustomField;
-use RT::Scrip;
-
-RT::LoadConfig();
+use warnings;
+use 5.010;
+
+use vars qw($Nobody $SystemUser $item);
+
+# fix lib paths, some may be relative
+BEGIN { # BEGIN RT CMD BOILERPLATE
+    require File::Spec;
+    require Cwd;
+    my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
+    my $bin_path;
+
+    for my $lib (@libs) {
+        unless ( File::Spec->file_name_is_absolute($lib) ) {
+            $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
+            $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+        }
+        unshift @INC, $lib;
+    }
+
+}
+
 use Term::ReadKey;
 use Getopt::Long;
+use Data::GUID;
 
-my %args;
+$| = 1; # unbuffer all output.
 
+my %args = (
+    package => 'RT',
+);
 GetOptions(
     \%args,
-    'prompt-for-dba-password', 'force', 'debug',
-    'action=s',                'dba=s', 'dba-password=s', 'datafile=s',
-    'datadir=s'
+    'action=s',
+    'force', 'debug',
+    'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
+    'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
+    'package=s', 'ext-version=s',
+    'upgrade-from=s', 'upgrade-to=s',
+    'help|h',
 );
 
-$| = 1;    #unbuffer that output.
-
-require RT::Handle;
-my $Handle = RT::Handle->new($RT::DatabaseType);
-$Handle->BuildDSN;
-my $dbh;
-
-if ( $args{'prompt-for-dba-password'} ) {
-    $args{'dba-password'} = get_dba_password();
-    chomp( $args{'dba-password'} );
+no warnings 'once';
+if ( $args{help} || ! $args{'action'} ) {
+    require Pod::Usage;
+    Pod::Usage::pod2usage({ verbose => 2 });
+    exit;
 }
 
-unless ( $args{'action'} ) {
-    help();
-    die;
+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( LogToSTDERR => 'warning')
+    unless ( RT->Config->Get( 'LogToSTDERR' )
+             && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ );
+RT::InitLogging();
+
+# 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;
 }
-if ( $args{'action'} eq 'init' ) {
-    $dbh = DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} )
-      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
-    print "Now creating a database for RT.\n";
-    if ($RT::DatabaseType ne 'Oracle' ||
-        $args{'dba'} ne $RT::DatabaseUser) {
-    create_db();
-    } else {
-        print "...skipped as ".$args{'dba'} ." is not " . $RT::DatabaseUser . " or we're working with Oracle.\n";
-    }
 
-    if ($RT::DatabaseType eq "mysql") {
-        # Check which version we're running
-        my ($version) = $dbh->selectrow_hashref("show variables like 'version'")->{Value} =~ /^(\d\.\d+)/;
-        print "*** Warning: RT is unsupported on MySQL versions before 4.0.x\n" if $version < 4;
-
-        # MySQL must have InnoDB support
-        my $innodb = $dbh->selectrow_hashref("show variables like 'have_innodb'")->{Value};
-        if ($innodb eq "NO") {
-            die "RT requires that MySQL be compiled with InnoDB table support.\n".
-              "See http://dev.mysql.com/doc/mysql/en/InnoDB.html\n";
-        } elsif ($innodb eq "DISABLED") {
-            die "RT requires that MySQL InnoDB table support be enabled.\n".
-              ($version < 4
-               ? "Add 'innodb_data_file_path=ibdata1:10M:autoextend' to the [mysqld] section of my.cnf\n"
-               : "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
-        }
+
+# check and setup @actions
+my @actions = grep $_, split /,/, $args{'action'};
+if ( @actions > 1 && $args{'datafile'} ) {
+    print STDERR "You can not use --datafile option with multiple actions.\n";
+    exit(-1);
+}
+foreach ( @actions ) {
+    unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) {
+        print STDERR "$0 called with an invalid --action parameter.\n";
+        exit(-1);
     }
-    
-    # SQLite can't deal with the disconnect/reconnect
-    unless ($RT::DatabaseType eq 'SQLite') {
-
-        $dbh->disconnect;
-
-       if ($RT::DatabaseType eq "Oracle") {
-        $RT::DatabasePassword = $RT::DatabasePassword; #Warning avidance
-        $dbh = DBI->connect( $Handle->DSN, ${RT::DatabaseUser}, ${RT::DatabasePassword} ) || die $DBI::errstr;
-       } else {
-       
-        $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} ) || die $DBI::errstr;
-       }
+    if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
+        print STDERR "You can not mix init, drop or upgrade action with any action.\n";
+        exit(-1);
     }
-    print "Now populating database schema.\n";
-    insert_schema();
-    print "Now inserting database ACLs\n";
-    insert_acl() unless ($RT::DatabaseType eq 'Oracle');
-    print "Now inserting RT core system objects\n";
-    insert_initial_data();
-    print "Now inserting RT data\n";
-    insert_data( $RT::EtcPath . "/initialdata" );
 }
-elsif ( $args{'action'} eq 'drop' ) {
-    unless ( $dbh =
-         DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
-    {
-        warn $DBI::errstr;
-        warn "Database doesn't appear to exist. Aborting database drop.";
-        exit(0);
+
+# convert init to multiple actions
+my $init = 0;
+if ( $actions[0] eq 'init' ) {
+    if ($args{'skip-create'}) {
+        @actions = qw(schema coredata insert);
+    } else {
+        @actions = qw(create schema acl coredata insert);
     }
-    drop_db();
-}
-elsif ( $args{'action'} eq 'insert_initial' ) {
-    insert_initial_data();
+    $init = 1;
 }
-elsif ( $args{'action'} eq 'insert' ) {
-    insert_data( $args{'datafile'} || ($args{'datadir'}."/content"));
-}
-elsif ($args{'action'} eq 'acl') {
-    $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
-      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
-     insert_acl($args{'datadir'});
+
+# set options from environment
+foreach my $key(qw(Type Host Name User Password)) {
+    next unless exists $ENV{ 'RT_DB_'. uc $key };
+    print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
+    RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
 }
-elsif ($args{'action'} eq 'schema') {
-    $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
-      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
-        insert_schema($args{'datadir'});
+
+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') || '';
+
+# load it here to get error immidiatly if DB type is not supported
+require RT::Handle;
+
+if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
+    $db_name = File::Spec->catfile($RT::VarPath, $db_name);
+    RT->Config->Set( DatabaseName => $db_name );
 }
 
-else {
-    print STDERR '$0 called with an invalid --action parameter';
-    exit(-1);
+my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
+my $dba_pass = exists($args{'dba-password'})
+                 ? $args{'dba-password'}
+                 : $ENV{'RT_DBA_PASSWORD'};
+
+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);
+    }
 }
 
-# {{{ sub insert_schema
-sub insert_schema {
-        my $base_path = (shift || $RT::EtcPath);
-    my (@schema);
-    print "Creating database schema.\n";
-
-    if ( -f $base_path . "/schema." . $RT::DatabaseType ) {
-       no warnings 'unopened';
-
-        open( SCHEMA, "<" . $base_path . "/schema." . $RT::DatabaseType );
-        open( SCHEMA_LOCAL, "<" . $RT::LocalEtcPath . "/schema." . $RT::DatabaseType );
-
-        my $statement = "";
-        foreach my $line (<SCHEMA>, ($_ = ';;'), <SCHEMA_LOCAL>) {
-            $line =~ s/\#.*//g;
-            $line =~ s/--.*//g;
-            $statement .= $line;
-            if ( $line =~ /;(\s*)$/ ) {
-                $statement =~ s/;(\s*)$//g;
-                push @schema, $statement;
-                $statement = "";
-            }
-        }
+my $version_word_regex = join '|', RT::Handle->version_words;
+my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
 
-       local $SIG{__WARN__} = sub {};
-       my $is_local = 0; # local/etc/schema needs to be nonfatal. 
-        $dbh->begin_work or die $dbh->errstr;
-        foreach my $statement (@schema) {
-           if ($statement =~ /^\s*;$/) { $is_local = 1; next; }
-            print STDERR "SQL: $statement\n" if defined $args{'debug'};
-            my $sth = $dbh->prepare($statement) or die $dbh->errstr;
-            unless ( $sth->execute or $is_local ) {
-                die "Problem with statement:\n $statement\n" . $sth->errstr;
-            }
-        }
-        $dbh->commit or die $dbh->errstr;
+print "Working with:\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";
 
-    }
-    else {
-        die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
-    }
-    print "Done setting up database schema.\n";
+my $package = $args{'package'} || 'RT';
+my $ext_version = $args{'ext-version'};
+my $full_id = Data::GUID->new->as_string;
+
+my $log_actions = 0;
+if ($args{'package'} ne 'RT') {
+    RT->ConnectToDatabase();
+    RT->InitSystemObjects();
+    $log_actions = 1;
+}
 
+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 "Done.\n";
 }
 
-# }}}
+sub action_create {
+    my %args = @_;
+    my $dbh = get_system_dbh();
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' );
+    return ($status, $msg) unless $status;
 
-# {{{ sub drop_db
-sub drop_db {
-    if ( $RT::DatabaseType eq 'Oracle' ) {
-        print <<END;
+    print "Now creating a $db_type database $db_name for RT.\n";
+    return RT::Handle->CreateDatabase( $dbh );
+}
 
-To delete the tables and sequences of the RT Oracle database by running 
-    \@etc/drop.Oracle 
-through SQLPlus.
+sub action_drop {
+    my %args = @_;
 
-END
-        return;
-    }  
+    print "Dropping $db_type database $db_name.\n";
     unless ( $args{'force'} ) {
         print <<END;
 
-About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
-WARNING: This will erase all data in $RT::DatabaseName.
+About to drop $db_type database $db_name on $db_host (port '$db_port').
+WARNING: This will erase all data in $db_name.
 
 END
-        exit unless _yesno();
-
+        exit(-2) unless _yesno();
     }
 
-    print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";
+    my $dbh = get_system_dbh();
+    return RT::Handle->DropDatabase( $dbh );
+}
 
-    if ( $RT::DatabaseType eq 'SQLite' ) {
-       unlink $RT::DatabaseName or warn $!;
-       return;
-    }
-    $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
+sub action_schema {
+    my %args = @_;
+    my $dbh = get_admin_dbh();
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' );
+    return ($status, $msg) unless $status;
+
+    my $individual_id = Data::GUID->new->as_string();
+    my %upgrade_data = (
+        action   => 'schema',
+        filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
+        stage    => 'before',
+        full_id  => $full_id,
+        individual_id => $individual_id,
+    );
+    $upgrade_data{'ext_version'} = $ext_version if $ext_version;
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
+
+    print "Now populating database schema.\n";
+    my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
+
+    %upgrade_data = (
+        stage         => 'after',
+        individual_id => $individual_id,
+        return_value  => [ @ret ],
+    );
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
+
+    return @ret;
 }
 
-# }}}
+sub action_acl {
+    my %args = @_;
+    my $dbh = get_admin_dbh();
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' );
+    return ($status, $msg) unless $status;
+
+    my $individual_id = Data::GUID->new->as_string();
+    my %upgrade_data = (
+        action   => 'acl',
+        filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
+        stage    => 'before',
+        full_id  => $full_id,
+        individual_id => $individual_id,
+    );
+    $upgrade_data{'ext_version'} = $ext_version if $ext_version;
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
+
+    print "Now inserting database ACLs.\n";
+    my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
+
+    %upgrade_data = (
+        stage         => 'after',
+        individual_id => $individual_id,
+        return_value  => [ @ret ],
+    );
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
+
+    return @ret;
+}
+
+sub action_indexes {
+    my %args = @_;
+    RT->ConnectToDatabase;
+    my $individual_id = Data::GUID->new->as_string();
+    my %upgrade_data = (
+        action   => 'indexes',
+        filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''),
+        stage    => 'before',
+        full_id  => $full_id,
+        individual_id => $individual_id,
+    );
+    $upgrade_data{'ext_version'} = $ext_version if $ext_version;
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
+
+    my $dbh = get_admin_dbh();
+    $RT::Handle = RT::Handle->new;
+    $RT::Handle->dbh( $dbh );
+    RT::InitLogging();
+
+    print "Now inserting database indexes.\n";
+    my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} );
+
+    $RT::Handle = RT::Handle->new;
+    $RT::Handle->dbh( undef );
+    RT->ConnectToDatabase;
+    %upgrade_data = (
+        stage         => 'after',
+        individual_id => $individual_id,
+        return_value  => [ @ret ],
+    );
+    RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions;
 
-# {{{ sub create_db
-sub create_db {
-    print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
-    if ( $RT::DatabaseType eq 'SQLite' ) {
-        return;
+    return @ret;
+}
+
+sub action_coredata {
+    my %args = @_;
+    $RT::Handle = RT::Handle->new;
+    $RT::Handle->dbh( undef );
+    RT::ConnectToDatabase();
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' );
+    return ($status, $msg) unless $status;
+
+    print "Now inserting RT core system objects.\n";
+    return $RT::Handle->InsertInitialData;
+}
+
+sub action_insert {
+    state $RAN_INIT;
+    my %args = @_;
+    unless ($RAN_INIT) {
+        $RT::Handle = RT::Handle->new;
+        RT::Init();
+        $RAN_INIT++;
+    }
+    $log_actions = 1;
+
+    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' );
+    return ($status, $msg) unless $status;
+
+    print "Now inserting data.\n";
+    my $file = $args{'datafile'};
+    $file = $RT::EtcPath . "/initialdata" if $init && !$file;
+    $file ||= $args{'datadir'}."/content";
+
+    my $individual_id = Data::GUID->new->as_string();
+    my %upgrade_data = (
+        action   => 'insert',
+        filename => Cwd::abs_path($file),
+        stage    => 'before',
+        full_id  => $full_id,
+        individual_id => $individual_id
+    );
+    $upgrade_data{'ext_version'} = $ext_version if $ext_version;
+
+    open my $handle, '<', $file or warn "Unable to open $file: $!";
+    $upgrade_data{content} = do {local $/; <$handle>} if $handle;
+
+    RT->System->AddUpgradeHistory($package => \%upgrade_data);
+
+    my @ret;
+
+    my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) };
+
+    for my $file (@{$args{backcompat} || []}) {
+        my $lines = do {local $/; local @ARGV = ($file); <>};
+        my $sub = eval "sub {\n# line 1 $file\n$lines\n}";
+        unless ($sub) {
+            warn "Failed to load backcompat $file: $@";
+            next;
+        }
+        my $current = $upgrade;
+        $upgrade = sub { $sub->($current) };
     }
-    elsif ( $RT::DatabaseType eq 'Pg' ) {
-        $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
-        if ($DBI::errstr) {
-            $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
+
+    $upgrade->();
+
+    # XXX Reconnecting to insert the history entry
+    # until we can sort out removing
+    # the disconnect at the end of InsertData.
+    RT->ConnectToDatabase();
+
+    %upgrade_data = (
+        stage         => 'after',
+        individual_id => $individual_id,
+        return_value  => [ @ret ],
+    );
+
+    RT->System->AddUpgradeHistory($package => \%upgrade_data);
+
+    my $db_type = RT->Config->Get('DatabaseType');
+    $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
+
+    return @ret;
+}
+
+sub action_upgrade {
+    my %args = @_;
+    my $base_dir = $args{'datadir'} || "./etc/upgrade";
+    return (0, "Couldn't read dir '$base_dir' with upgrade data")
+        unless -d $base_dir || -r _;
+
+    my $upgrading_from = undef;
+    do {
+        if ( defined $upgrading_from ) {
+            print "Doesn't match #.#.#: ";
+        } else {
+            print "Enter $args{package} version you're upgrading from: ";
         }
+        $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>;
+        chomp $upgrading_from;
+        $upgrading_from =~ s/\s+//g;
+    } while $upgrading_from !~ /$version_dir/;
+
+    my $upgrading_to = $RT::VERSION;
+    return (0, "The current version $upgrading_to is lower than $upgrading_from")
+        if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;
+
+    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, 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];
     }
-    elsif ($RT::DatabaseType eq 'Oracle') {
-        insert_acl();
+
+    print "\nGoing to apply following upgrades:\n";
+    print map "* $_\n", @versions;
+
+    {
+        my $custom_upgrading_to = undef;
+        do {
+            if ( defined $custom_upgrading_to ) {
+                print "Doesn't match #.#.#: ";
+            } else {
+                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 = $args{'upgrade-to'} || scalar <STDIN>;
+            chomp $custom_upgrading_to;
+            $custom_upgrading_to =~ s/\s+//g;
+            last unless $custom_upgrading_to;
+        } while $custom_upgrading_to !~ /$version_dir/;
+
+        if ( $custom_upgrading_to ) {
+            return (
+                0, "The version you entered ($custom_upgrading_to) is lower than\n"
+                ."version you're upgrading from ($upgrading_from)"
+            ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;
+
+            return (1, "The version you're upgrading to is up to date")
+                if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;
+
+            if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
+                print "Version you entered is greater than installed ($RT::VERSION).\n";
+                _yesno() or exit(-2);
+            }
+            # ok, checked everything no let's refresh list
+            $upgrading_to = $custom_upgrading_to;
+            @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);
+
+            return (1, "No DB changes between $upgrading_from and $upgrading_to")
+                unless @versions;
+
+            print "\nGoing to apply following upgrades:\n";
+            print map "* $_\n", @versions;
+        }
     }
-    elsif ( $RT::DatabaseType eq 'Informix' ) {
-       $ENV{DB_LOCALE} = 'en_us.utf8';
-        $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
+
+    unless ( $args{'force'} ) {
+        print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
+        _yesno() or exit(-2);
+    }
+
+    RT->ConnectToDatabase();
+    RT->InitSystemObjects();
+    $log_actions = 1;
+
+    RT->System->AddUpgradeHistory($package => {
+        type      => 'full upgrade',
+        action    => 'upgrade',
+        stage     => 'before',
+        from      => $upgrading_from,
+        to        => $upgrading_to,
+        versions  => [@versions],
+        full_id => $full_id,
+        individual_id => $full_id
+    });
+
+    # Ensure that the Attributes column is big enough to hold the
+    # upgrade steps we're going to add; this step exists in 4.0.6 for
+    # mysql, but that may be too late.  Run it as soon as possible.
+    if (RT->Config->Get('DatabaseType') eq 'mysql'
+            and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) {
+        my $dbh = get_admin_dbh();
+        # Before the binary switch in 3.7.87, we want to alter text ->
+        # longtext, not blob -> longblob
+        if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) {
+            $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT")
+        } else {
+            $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB")
+        }
     }
-    else {
-        $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
+
+    my $previous = $upgrading_from;
+    my ( $ret, $msg );
+    foreach my $n ( 0..$#versions ) {
+        my $v = $versions[$n];
+        my $individual_id = Data::GUID->new->as_string();
+
+        my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions;
+        print "Processing $v\n";
+
+        RT->System->AddUpgradeHistory($package => {
+            action => 'upgrade',
+            type   => 'individual upgrade',
+            stage  => 'before',
+            from   => $previous,
+            to     => $v,
+            full_id => $full_id,
+            individual_id => $individual_id,
+        });
+
+        my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back);
+
+        if ( -e "$base_dir/$v/schema.$db_type" ) {
+            ( $ret, $msg ) = action_schema( %tmp );
+            return ( $ret, $msg ) unless $ret;
+        }
+        if ( -e "$base_dir/$v/acl.$db_type" ) {
+            ( $ret, $msg ) = action_acl( %tmp );
+            return ( $ret, $msg ) unless $ret;
+        }
+        if ( -e "$base_dir/$v/indexes" ) {
+            ( $ret, $msg ) = action_indexes( %tmp );
+            return ( $ret, $msg ) unless $ret;
+        }
+        if ( -e "$base_dir/$v/content" ) {
+            ( $ret, $msg ) = action_insert( %tmp );
+            return ( $ret, $msg ) unless $ret;
+        }
+
+        # XXX: Another connect since the insert called
+        # previous to this step will disconnect.
+
+        RT->ConnectToDatabase();
+
+        RT->System->AddUpgradeHistory($package => {
+            stage         => 'after',
+            individual_id => $individual_id,
+        });
+
+        $previous = $v;
     }
+
+    RT->System->AddUpgradeHistory($package => {
+        stage         => 'after',
+        individual_id => $full_id,
+    });
+
+    return 1;
 }
 
-# }}}
+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/$_" && /$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 defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
+        grep RT::Handle::cmp_version($_, $from) > 0,
+        sort RT::Handle::cmp_version @versions;
+}
+
+sub error {
+    my ($action, $msg) = @_;
+    print STDERR "Couldn't finish '$action' step.\n\n";
+    print STDERR "ERROR: $msg\n\n";
+    exit(-1);
+}
 
 sub get_dba_password {
-    print "In order to create or update your RT database,";
-    print "this script needs to connect to your "
-      . $RT::DatabaseType
-      . " instance on "
-      . $RT::DatabaseHost . " as "
-      . $args{'dba'} . ".\n";
+    print "In order to create or update your RT database,"
+        . " this script needs to connect to your "
+        . " $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: ";
@@ -304,382 +628,175 @@ sub get_dba_password {
     return ($password);
 }
 
-# {{{ sub _yesno
-sub _yesno {
-    print "Proceed [y/N]:";
-    my $x = scalar(<STDIN>);
-    $x =~ /^y/i;
+#   get_system_dbh
+#   Returns L<DBI> database handle connected to B<system> with DBA credentials.
+#   See also L<RT::Handle/SystemDSN>.
+
+
+sub get_system_dbh {
+    return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
 }
 
-# }}}
+sub get_admin_dbh {
+    return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
+}
 
-# {{{ insert_acls
-sub insert_acl {
+# get_rt_dbh [USER, PASSWORD]
 
-        my $base_path = (shift || $RT::EtcPath);
+# 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.
 
-    if ( $RT::DatabaseType =~ /^oracle$/i ) {
-        do $base_path . "/acl.Oracle"
-          || die "Couldn't find ACLS for Oracle\n" . $@;
-    }
-    elsif ( $RT::DatabaseType =~ /^pg$/i ) {
-        do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
-    }
-    elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
-        do $base_path . "/acl.mysql"
-          || die "Couldn't find ACLS for mysql in $base_path\n" . $@;
-    }
-    elsif ( $RT::DatabaseType =~ /^Sybase$/i ) {
-        do $base_path . "/acl.Sybase"
-          || die "Couldn't find ACLS for Sybase in $base_path\n" . $@;
-    }
-    elsif ( $RT::DatabaseType =~ /^informix$/i ) {
-        do $base_path . "/acl.Informix"
-          || die "Couldn't find ACLS for Informix in $base_path\n" . $@;
-    }
-    elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
-        return;
-    }
-    else {
-        die "Unknown RT database type";
-    }
+sub get_rt_dbh {
+    return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
+}
 
-    my @acl = acl($dbh);
-    foreach my $statement (@acl) {
-        print STDERR $statement if $args{'debug'};
-        my $sth = $dbh->prepare($statement) or die $dbh->errstr;
-        unless ( $sth->execute ) {
-            die "Problem with statement:\n $statement\n" . $sth->errstr;
+sub _get_dbh {
+    my ($dsn, $user, $pass) = @_;
+    my $dbh = DBI->connect(
+        $dsn, $user, $pass,
+        { RaiseError => 0, PrintError => 0 },
+    );
+    unless ( $dbh ) {
+        my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
+        if ( $args{'debug'} ) {
+            require Carp; Carp::confess( $msg );
+        } else {
+            print STDERR $msg; exit -1;
         }
     }
-    print "Done setting up database ACLs.\n";
+    return $dbh;
 }
 
-# }}}
+sub _yesno {
+    print "Proceed [y/N]:";
+    my $x = scalar(<STDIN>);
+    $x =~ /^y/i;
+}
 
-=head2 get_system_dsn
+1;
 
-Returns a dsn suitable for database creates and drops
-and user creates and drops
+__END__
 
-=cut
+=head1 NAME
 
-sub get_system_dsn {
+rt-setup-database - Set up RT's database
 
-    my $dsn = $Handle->DSN;
+=head1 SYNOPSIS
 
-    #with mysql, you want to connect sans database to funge things
-    if ( $RT::DatabaseType eq 'mysql' ) {
-        $dsn =~ s/dbname=$RT::DatabaseName//;
+    rt-setup-database --action ... 
 
-        # with postgres, you want to connect to database1
-    }
-    elsif ( $RT::DatabaseType eq 'Pg' ) {
-        $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
-    }
-    elsif ( $RT::DatabaseType eq 'Informix' ) {
-       # with Informix, you want to connect sans database:
-       $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
-    }
-    return $dsn;
-}
+=head1 OPTIONS
 
-sub insert_initial_data {
+=over
 
-    RT::InitLogging();
+=item action
 
-    #connect to the db, for actual RT work
-    require RT::Handle;
-    $RT::Handle = RT::Handle->new();
-    $RT::Handle->Connect();
+Several actions can be combined using comma separated list.
 
-    #Put together a current user object so we can create a User object
-    my $CurrentUser = new RT::CurrentUser();
+=over
 
-    print "Checking for existing system user...";
-    my $test_user = RT::User->new($CurrentUser);
-    $test_user->Load('RT_System');
-    if ( $test_user->id ) {
-        print "found!\n\nYou appear to have a functional RT database.\n"
-          . "Exiting, so as not to clobber your existing data.\n";
-        exit(-1);
+=item init
 
-    }
-    else {
-        print "not found.  This appears to be a new installation.\n";
-    }
+Initialize the database. This is combination of multiple actions listed below.
+Create DB, schema, setup acl, insert core data and initial data.
 
-    print "Creating system user...";
-    my $RT_System = new RT::User($CurrentUser);
+=item upgrade
 
-    my ( $val, $msg ) = $RT_System->_BootstrapCreate(
-        Name     => 'RT_System',
-        RealName => 'The RT System itself',
-        Comments =>
-'Do not delete or modify this user. It is integral to RT\'s internal database structures',
-        Creator => '1',
-        LastUpdatedBy => '1' );
+Apply all needed schema/acl/content updates (will ask for version to upgrade
+from)
 
-    unless ($val) {
-        print "$msg\n";
-        exit(1);
-    }
-    print "done.\n";
-    $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
+=item create
 
-}
+Create the database.
 
-# load some sort of data into the database
+=item drop
 
-sub insert_data {
-    my $datafile = shift;
+Drop the database.  This will B<ERASE ALL YOUR DATA>.
 
-    #Connect to the database and get RT::SystemUser and RT::Nobody loaded
-    RT::Init;
+=item schema
 
-    my $CurrentUser = RT::CurrentUser->new();
-    $CurrentUser->LoadByName('RT_System');
+Initialize only the database schema
 
-    if ( $datafile eq $RT::EtcPath . "/initialdata" ) {
+To use a local or supplementary datafile, specify it using the '--datadir'
+option below.
 
-        print "Creating Superuser  ACL...";
+=item acl
 
-        my $superuser_ace = RT::ACE->new($CurrentUser);
-        $superuser_ace->_BootstrapCreate(
-                             PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
-                             PrincipalType => 'Group',
-                             RightName     => 'SuperUser',
-                             ObjectType    => 'RT::System',
-                             ObjectId      => '1' );
+Initialize only the database ACLs
 
-        print "done.\n";
-    }
+To use a local or supplementary datafile, specify it using the '--datadir'
+option below.
 
-    # Slurp in stuff to insert from the datafile. Possible things to go in here:-
-    # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates
+=item coredata 
 
-    require $datafile
-      || die "Couldn't find initial data for import\n" . $@;
+Insert data into RT's database. This data is required for normal functioning of
+any RT instance.
 
-    if (@Groups) {
-        print "Creating groups...";
-        foreach $item (@Groups) {
-            my $new_entry = RT::Group->new($CurrentUser);
-            my ( $return, $msg ) = $new_entry->_Create(%$item);
-            print "(Error: $msg)" unless ($return);
-            print $return. ".";
-        }
-        print "done.\n";
-    }
-    if (@Users) {
-        print "Creating users...";
-        foreach $item (@Users) {
-            my $new_entry = new RT::User($CurrentUser);
-            my ( $return, $msg ) = $new_entry->Create(%$item);
-            print "(Error: $msg)" unless ($return);
-            print $return. ".";
-        }
-        print "done.\n";
-    }
-    if (@Queues) {
-        print "Creating queues...";
-        for $item (@Queues) {
-            my $new_entry = new RT::Queue($CurrentUser);
-            my ( $return, $msg ) = $new_entry->Create(%$item);
-            print "(Error: $msg)" unless ($return);
-            print $return. ".";
-        }
-        print "done.\n";
-    }
-    if (@ACL) {
-        print "Creating ACL...";
-        for my $item (@ACL) {
-
-           my ($princ, $object);
-
-           # Global rights or Queue rights?
-           if ($item->{'Queue'}) {
-                $object = RT::Queue->new($CurrentUser);
-                $object->Load( $item->{'Queue'} );
-           } else {
-               $object = $RT::System;
-           }
-
-           # Group rights or user rights?
-           if ($item->{'GroupDomain'}) {
-                $princ = RT::Group->new($CurrentUser);
-               if ($item->{'GroupDomain'} eq 'UserDefined') {
-                  $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
-               } elsif ($item->{'GroupDomain'} eq 'SystemInternal') {
-                  $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
-               } elsif ($item->{'GroupDomain'} eq 'RT::System-Role') {
-                  $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
-               } elsif ($item->{'GroupDomain'} eq 'RT::Queue-Role' &&
-                        $item->{'Queue'}) {
-                  $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
-                                             Queue => $object->id);
-               } else {
-                  $princ->Load( $item->{'GroupId'} );
-               }
-           } else {
-               $princ = RT::User->new($CurrentUser);
-               $princ->Load( $item->{'UserId'} );
-           }
-
-           # Grant it
-           my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
-                                                     Right => $item->{'Right'},
-                                                     Object => $object );
-
-            if ($return) {
-                print $return. ".";
-            }
-            else {
-                print $msg . ".";
+=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.
 
-        }
-        print "done.\n";
-    }
-    if (@CustomFields) {
-        print "Creating custom fields...";
-        for $item (@CustomFields) {
-            my $new_entry = new RT::CustomField($CurrentUser);
-            my $values    = $item->{'Values'};
-            delete $item->{'Values'};
-            my $q     = $item->{'Queue'};
-            my $q_obj = RT::Queue->new($CurrentUser);
-            $q_obj->Load($q);
-            if ( $q_obj->Id ) {
-                $item->{'Queue'} = $q_obj->Id;
-            }
-            elsif ( $q == 0 ) {
-                $item->{'Queue'} = 0;
-            }
-            else {
-                print "(Error: Could not find queue " . $q . ")\n"
-                  unless ( $q_obj->Id );
-                next;
-            }
-            my ( $return, $msg ) = $new_entry->Create(%$item);
+=back
 
-            foreach my $value ( @{$values} ) {
-                my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
-                print "(Error: $emsg)\n" unless ($eval);
-            }
+=item datafile
 
-            print "(Error: $msg)\n" unless ($return);
-            print $return. ".";
-        }
+file path of the data you want to action on
 
-        print "done.\n";
-    }
+e.g. C<--datafile /path/to/datafile>
 
-    if (@ScripActions) {
-        print "Creating ScripActions...";
+=item datadir
 
-        for $item (@ScripActions) {
-            my $new_entry = RT::ScripAction->new($CurrentUser);
-            my $return    = $new_entry->Create(%$item);
-            print $return. ".";
-        }
+Used to specify a path to find the local database schema and acls to be
+installed.
 
-        print "done.\n";
-    }
+e.g. C<--datadir /path/to/>
 
-    if (@ScripConditions) {
-        print "Creating ScripConditions...";
+=item dba
 
-        for $item (@ScripConditions) {
-            my $new_entry = RT::ScripCondition->new($CurrentUser);
-            my $return    = $new_entry->Create(%$item);
-            print $return. ".";
-        }
+dba's username
 
-        print "done.\n";
-    }
+=item dba-password
 
-    if (@Templates) {
-        print "Creating templates...";
+dba's password
 
-        for $item (@Templates) {
-            my $new_entry = new RT::Template($CurrentUser);
-            my $return    = $new_entry->Create(%$item);
-            print $return. ".";
-        }
-        print "done.\n";
-    }
-    if (@Scrips) {
-        print "Creating scrips...";
-
-        for $item (@Scrips) {
-            my $new_entry = new RT::Scrip($CurrentUser);
-            my ( $return, $msg ) = $new_entry->Create(%$item);
-            if ($return) {
-                print $return. ".";
-            }
-            else {
-                print "(Error: $msg)\n";
-            }
-        }
-        print "done.\n";
-    }
-    $RT::Handle->Disconnect() unless ($RT::DatabaseType eq 'SQLite');
-    print "Done setting up database content.\n";
-}
+=item prompt-for-dba-password
 
-=head2 ACLEquivGroupId
+Ask for the database administrator's password interactively
 
-Given a userid, return that user's acl equivalence group
+=item skip-create
 
-=cut
+for 'init': skip creating the database and the user account, so we don't need
+administrator privileges
 
-sub ACLEquivGroupId {
-    my $username = shift;
-    my $user     = RT::User->new($RT::SystemUser);
-    $user->Load($username);
-    my $equiv_group = RT::Group->new($RT::SystemUser);
-    $equiv_group->LoadACLEquivalenceGroup($user);
-    return ( $equiv_group->Id );
-}
+=item root-password-file
 
-sub help {
+for 'init' and 'insert': rather than using the default administrative password
+for RT's "root" user, use the password in this file.
 
-    print <<EOF;
+=item package 
 
-$0: Set up RT's database
+the name of the entity performing a create or upgrade. Used for logging changes
+in the DB. Defaults to RT, otherwise it should be the fully qualified package name
+of the extension or plugin making changes to the DB.
 
---action        init    Initialize the database
-                drop    Drop the database. 
-                        This will ERASE ALL YOUR DATA
-                insert_initial 
-                        Insert RT's core system objects
-                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.
-                        
-                acl     Initialize only the database ACLs
-                        To use a local or supplementary datafile, specify it
-                        using the '--datadir' option below.
-                        
-                schema  Initialize only the database schema
-                        To use a local or supplementary datafile, specify it
-                        using the '--datadir' option below.
+=item ext-version
 
---datafile /path/to/datafile
---datadir /path/to/              Used to specify a path to find the local
-                                database schema and acls to be installed.
+current version of extension making a change. Not needed for RT since RT has a
+more elaborate system to track upgrades across multiple versions.
 
+=item upgrade-from
 
---dba                           dba's username
---dba-password                  dba's password
---prompt-for-dba-password       Ask for the database administrator's password interactively
+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
 
-EOF
+for 'upgrade': specifies the version to upgrade to, and do not prompt
+for it if it appears to be a valid version.
 
-}
+=back
 
-1;
+=cut