lib/msf/core/db_manager/migration.rb
# -*- coding: binary -*-
module Msf::DBManager::Migration
# Loads Metasploit Data Models and adds gathers migration paths.
#
# @return Array[String]
def add_rails_engine_migration_paths
unless defined? ActiveRecord
fail "Bundle installed '--without #{Bundler.settings.without.join(' ')}'. To clear the without option do " \
"`bundle install --without ''` (the --without flag with an empty string) or `rm -rf .bundle` to remove " \
"the .bundle/config manually and then `bundle install`"
end
gather_engine_migration_paths
end
# Migrate database to latest schema version.
#
# @param config [Hash] see ActiveRecord::Base.establish_connection
# @param verbose [Boolean] see ActiveRecord::Migration.verbose
# @return [Array<ActiveRecord::MigrationProxy] List of migrations that
# ran.
#
# @see ActiveRecord::MigrationContext.migrate
def migrate(config=nil, verbose=false)
ran = []
# Rails 5 changes ActiveRecord parents means to migrate outside
# the `rake` task framework has to dig a little lower into ActiveRecord
# to set up the DB connection capable of interacting with migration.
previouslyConnected = ActiveRecord::Base.connected?
unless previouslyConnected
ApplicationRecord.remove_connection
ActiveRecord::Base.establish_connection(config)
end
ActiveRecord::Migration.verbose = verbose
ActiveRecord::Base.connection_pool.with_connection do
begin
context = default_migration_context
if needs_migration?(context)
ran = context.migrate
end
# ActiveRecord::Migrator#migrate rescues all errors and re-raises them
# as StandardError
rescue StandardError => error
self.error = error
elog('DB.migrate threw an exception', error: error)
end
end
unless previouslyConnected
ActiveRecord::Base.remove_connection
ApplicationRecord.establish_connection(config)
end
# Since the connections that existed before the migrations ran could
# have outdated column information, reset column information for all
# ApplicationRecord descendents to prevent missing method errors for
# column methods for columns created in migrations after the column
# information was cached.
reset_column_information
return ran
end
# Determine if the currently established database connection needs migration
#
# @param [ActiveRecord::MigrationContext,snil] context The migration context to check. Will default if not supplied
# @return [Boolean] True if migration is required, false otherwise
def needs_migration?(context = default_migration_context)
ActiveRecord::Base.connection_pool.with_connection do
return context.needs_migration?
end
end
# Flag to indicate database migration has completed
#
# @return [Boolean,nil]
attr_accessor :migrated
private
# @return [ActiveRecord::MigrationContext]
def default_migration_context
ActiveRecord::MigrationContext.new(gather_engine_migration_paths, ActiveRecord::SchemaMigration)
end
# Loads gathers migration paths from all loaded Rails engines.
#
# @return Array[String]
def gather_engine_migration_paths
paths = ActiveRecord::Migrator.migrations_paths
::Rails::Engine.subclasses.map(&:instance).each.each do |engine|
migrations_paths = engine.paths['db/migrate'].existent_directories
migrations_paths.each do |migrations_path|
# Since ActiveRecord::Migrator.migrations_paths can persist between
# instances of Msf::DBManager, such as in specs,
# migrations_path may already be part of
# migrations_paths, in which case it should not be added or multiple
# migrations with the same version number errors will occur.
unless paths.include? migrations_path
paths << migrations_path
end
end
end
paths
end
# Resets the column information for all descendants of ApplicationRecord
# since some of the migrations may have cached column information that
# has been updated by later migrations.
#
# @return [void]
def reset_column_information
ApplicationRecord.descendants.each do |descendant|
descendant.reset_column_information
end
end
end