Skip to content

Commit 882c550

Browse files
committed
Merge pull request rapid7#1852 from limhoff-r7/bug/migrations
[Delivers #50179803]
2 parents 604da84 + 89bd5b4 commit 882c550

File tree

4 files changed

+206
-33
lines changed

4 files changed

+206
-33
lines changed

lib/msf/core/db_manager.rb

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'msf/base/config'
44
require 'msf/core'
55
require 'msf/core/db'
6+
require 'msf/core/db_manager/migration'
67
require 'msf/core/task_manager'
78
require 'fileutils'
89
require 'shellwords'
@@ -17,6 +18,9 @@ module Msf
1718
###
1819

1920
class DBManager
21+
# Provides :framework and other accessors
22+
include Msf::DBManager::Migration
23+
include Msf::Framework::Offspring
2024

2125
# Mainly, it's Ruby 1.9.1 that cause a lot of problems now, along with Ruby 1.8.6.
2226
# Ruby 1.8.7 actually seems okay, but why tempt fate? Let's say 1.9.3 and beyond.
@@ -28,9 +32,6 @@ def warn_about_rubies
2832
end
2933
end
3034

31-
# Provides :framework and other accessors
32-
include Framework::Offspring
33-
3435
# Returns true if we are ready to load/store data
3536
def active
3637
return false if not @usable
@@ -53,9 +54,6 @@ def active
5354
# Stores a TaskManager for serializing database events
5455
attr_accessor :sink
5556

56-
# Flag to indicate database migration has completed
57-
attr_accessor :migrated
58-
5957
# Flag to indicate that modules are cached
6058
attr_accessor :modules_cached
6159

@@ -287,33 +285,6 @@ def disconnect
287285
end
288286
end
289287

290-
# Migrate database to latest schema version.
291-
#
292-
# @param verbose [Boolean] see ActiveRecord::Migration.verbose
293-
# @return [Array<ActiveRecord::MigrationProxy] List of migrations that ran.
294-
#
295-
# @see ActiveRecord::Migrator.migrate
296-
def migrate(verbose=false)
297-
ran = []
298-
ActiveRecord::Migration.verbose = verbose
299-
300-
ActiveRecord::Base.connection_pool.with_connection do
301-
begin
302-
ran = ActiveRecord::Migrator.migrate(
303-
ActiveRecord::Migrator.migrations_paths
304-
)
305-
# ActiveRecord::Migrator#migrate rescues all errors and re-raises them as
306-
# StandardError
307-
rescue StandardError => error
308-
self.error = error
309-
elog("DB.migrate threw an exception: #{error}")
310-
dlog("Call stack:\n#{error.backtrace.join "\n"}")
311-
end
312-
end
313-
314-
return ran
315-
end
316-
317288
def workspace=(workspace)
318289
@workspace_name = workspace.name
319290
end

lib/msf/core/db_manager/migration.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module Msf
2+
class DBManager
3+
module Migration
4+
# Migrate database to latest schema version.
5+
#
6+
# @param verbose [Boolean] see ActiveRecord::Migration.verbose
7+
# @return [Array<ActiveRecord::MigrationProxy] List of migrations that
8+
# ran.
9+
#
10+
# @see ActiveRecord::Migrator.migrate
11+
def migrate(verbose=false)
12+
ran = []
13+
ActiveRecord::Migration.verbose = verbose
14+
15+
ActiveRecord::Base.connection_pool.with_connection do
16+
begin
17+
ran = ActiveRecord::Migrator.migrate(
18+
ActiveRecord::Migrator.migrations_paths
19+
)
20+
# ActiveRecord::Migrator#migrate rescues all errors and re-raises them
21+
# as StandardError
22+
rescue StandardError => error
23+
self.error = error
24+
elog("DB.migrate threw an exception: #{error}")
25+
dlog("Call stack:\n#{error.backtrace.join "\n"}")
26+
end
27+
end
28+
29+
# Since the connections that existed before the migrations ran could
30+
# have outdated column information, reset column information for all
31+
# ActiveRecord::Base descendents to prevent missing method errors for
32+
# column methods for columns created in migrations after the column
33+
# information was cached.
34+
reset_column_information
35+
36+
return ran
37+
end
38+
39+
# Flag to indicate database migration has completed
40+
#
41+
# @return [Boolean]
42+
attr_accessor :migrated
43+
44+
private
45+
46+
# Resets the column information for all descendants of ActiveRecord::Base
47+
# since some of the migrations may have cached column information that
48+
# has been updated by later migrations.
49+
#
50+
# @return [void]
51+
def reset_column_information
52+
ActiveRecord::Base.descendants.each do |descendant|
53+
descendant.reset_column_information
54+
end
55+
end
56+
end
57+
end
58+
end

spec/lib/msf/db_manager_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
db_manager
1919
end
2020

21+
it_should_behave_like 'Msf::DBManager::Migration'
2122
it_should_behave_like 'Msf::DBManager::ImportMsfXml'
2223

2324
context '#initialize_metasploit_data_models' do
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
shared_examples_for 'Msf::DBManager::Migration' do
2+
it { should be_a Msf::DBManager::Migration }
3+
4+
context '#migrate' do
5+
def migrate
6+
db_manager.migrate
7+
end
8+
9+
it 'should create a connection' do
10+
ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice
11+
12+
migrate
13+
end
14+
15+
it 'should call ActiveRecord::Migrator.migrate' do
16+
ActiveRecord::Migrator.should_receive(:migrate).with(
17+
ActiveRecord::Migrator.migrations_paths
18+
)
19+
20+
migrate
21+
end
22+
23+
it 'should return migrations that were ran from ActiveRecord::Migrator.migrate' do
24+
migrations = [mock('Migration 1')]
25+
ActiveRecord::Migrator.stub(:migrate => migrations)
26+
27+
migrate.should == migrations
28+
end
29+
30+
it 'should reset the column information' do
31+
db_manager.should_receive(:reset_column_information)
32+
33+
migrate
34+
end
35+
36+
context 'with StandardError from ActiveRecord::Migration.migrate' do
37+
let(:error) do
38+
StandardError.new(message)
39+
end
40+
41+
let(:message) do
42+
"Error during migration"
43+
end
44+
45+
before(:each) do
46+
ActiveRecord::Migrator.stub(:migrate).and_raise(error)
47+
end
48+
49+
it 'should set Msf::DBManager#error' do
50+
migrate
51+
52+
db_manager.error.should == error
53+
end
54+
55+
it 'should log error message at error level' do
56+
db_manager.should_receive(:elog) do |error_message|
57+
error_message.should include(error.to_s)
58+
end
59+
60+
migrate
61+
end
62+
63+
it 'should log error backtrace at debug level' do
64+
db_manager.should_receive(:dlog) do |debug_message|
65+
debug_message.should include('Call stack')
66+
end
67+
68+
migrate
69+
end
70+
end
71+
72+
context 'with verbose' do
73+
def migrate
74+
db_manager.migrate(verbose)
75+
end
76+
77+
context 'false' do
78+
let(:verbose) do
79+
false
80+
end
81+
82+
it 'should set ActiveRecord::Migration.verbose to false' do
83+
ActiveRecord::Migration.should_receive(:verbose=).with(verbose)
84+
85+
migrate
86+
end
87+
end
88+
89+
context 'true' do
90+
let(:verbose) do
91+
true
92+
end
93+
94+
it 'should set ActiveRecord::Migration.verbose to true' do
95+
ActiveRecord::Migration.should_receive(:verbose=).with(verbose)
96+
97+
migrate
98+
end
99+
end
100+
end
101+
102+
context 'without verbose' do
103+
it 'should set ActiveRecord::Migration.verbose to false' do
104+
ActiveRecord::Migration.should_receive(:verbose=).with(false)
105+
106+
db_manager.migrate
107+
end
108+
end
109+
end
110+
111+
context '#migrated' do
112+
it { should respond_to :migrated }
113+
it { should respond_to :migrated= }
114+
end
115+
116+
context '#reset_column_information' do
117+
def reset_column_information
118+
db_manager.send(:reset_column_information)
119+
end
120+
121+
it 'should use ActiveRecord::Base.descendants to find both direct and indirect subclasses' do
122+
ActiveRecord::Base.should_receive(:descendants).and_return([])
123+
124+
reset_column_information
125+
end
126+
127+
it 'should reset column information on each descendant of ActiveRecord::Base' do
128+
descendants = []
129+
130+
1.upto(2) do |i|
131+
descendants << mock("Descendant #{i}")
132+
end
133+
134+
ActiveRecord::Base.stub(:descendants => descendants)
135+
136+
descendants.each do |descendant|
137+
descendant.should_receive(:reset_column_information)
138+
end
139+
140+
reset_column_information
141+
end
142+
end
143+
end

0 commit comments

Comments
 (0)