myreplicator 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/app/assets/stylesheets/myreplicator/application.css +53 -10
  2. data/app/controllers/myreplicator/exports_controller.rb +17 -1
  3. data/app/controllers/myreplicator/home_controller.rb +4 -1
  4. data/app/helpers/myreplicator/application_helper.rb +20 -0
  5. data/app/models/myreplicator/export.rb +108 -23
  6. data/app/models/myreplicator/log.rb +90 -0
  7. data/app/views/myreplicator/exports/_form.html.erb +33 -9
  8. data/app/views/myreplicator/home/_home_menu.erb +6 -2
  9. data/app/views/myreplicator/home/errors.html.erb +15 -5
  10. data/app/views/myreplicator/home/index.html.erb +20 -43
  11. data/db/migrate/20121025191622_create_myreplicator_exports.rb +0 -6
  12. data/db/migrate/20121212003652_create_myreplicator_logs.rb +21 -0
  13. data/lib/exporter/export_metadata.rb +8 -1
  14. data/lib/exporter/mysql_exporter.rb +32 -0
  15. data/lib/exporter/sql_commands.rb +85 -4
  16. data/lib/loader/loader.rb +48 -7
  17. data/lib/myreplicator.rb +16 -4
  18. data/lib/myreplicator/version.rb +1 -1
  19. data/lib/transporter/parallelizer.rb +8 -0
  20. data/lib/transporter/transporter.rb +75 -11
  21. data/test/dummy/config/myreplicator.yml +0 -2
  22. data/test/dummy/config/myreplicator.yml~ +2 -0
  23. data/test/dummy/db/migrate/{20121108204327_create_myreplicator_exports.myreplicator.rb~ → 20121213003552_create_myreplicator_exports.myreplicator.rb} +3 -1
  24. data/test/dummy/db/migrate/20121213003553_create_myreplicator_logs.myreplicator.rb +22 -0
  25. data/test/dummy/db/schema.rb +24 -7
  26. data/test/dummy/log/development.log +1083 -0
  27. data/test/dummy/tmp/cache/assets/CD5/B90/sprockets%2Fc999d13a6a21113981c0d820e8043bdf +0 -0
  28. data/test/dummy/tmp/cache/assets/D8B/B60/sprockets%2Faa32227c440a378ccd21218eefeb80bf +0 -0
  29. data/test/dummy/tmp/cache/assets/DF8/5D0/sprockets%2Fb815ed34d61cfed96222daa3bfd1d84d +0 -0
  30. data/test/dummy/tmp/myreplicator/okl_test_batchy_batches_1355432256.tsv.gz +0 -0
  31. data/test/unit/myreplicator/log_test.rb +9 -0
  32. metadata +14 -8
  33. data/test/dummy/db/migrate/20121115194022_create_myreplicator_exports.myreplicator.rb +0 -36
@@ -129,7 +129,7 @@ html { overflow-y:scroll;overflow-x:auto; }
129
129
  padding-left:20px;
130
130
  margin-top:-10px;
131
131
  }
132
- .home-menu span {
132
+ .home-menu > span {
133
133
  color:#474747;
134
134
  display:block;
135
135
  float:left;
@@ -150,6 +150,7 @@ html { overflow-y:scroll;overflow-x:auto; }
150
150
  letter-spacing:1px;
151
151
  margin:8px 15px 8px 0;
152
152
  padding:3px 12px;
153
+ position:relative;
153
154
  text-decoration:none;
154
155
  -webkit-transition: all 0.25s ease-in-out;
155
156
  -moz-transition: all 0.25s ease-in-out;
@@ -178,7 +179,31 @@ html { overflow-y:scroll;overflow-x:auto; }
178
179
  text-shadow: 0 1px #f9f9f9;
179
180
  }
180
181
  .home-menu a:active {background:#e0e0e0;}
181
-
182
+ .home-menu a span {
183
+ background:#990000;
184
+ box-shadow:0 0 3px rgba(0,0,0,0.4);
185
+ border-radius:999px;
186
+ color:#fff;
187
+ font-size:11px;
188
+ font-weight:normal;
189
+ height:13px;
190
+ display:block;
191
+ opacity:0;
192
+ padding:0 0 3px 1px;
193
+ position:absolute;
194
+ top:-7px;
195
+ right:-5px;
196
+ text-align:center;
197
+ text-shadow:1px 1px #333;
198
+ width:13px;
199
+ -webkit-transition: all 0.25s ease-in-out;
200
+ -moz-transition: all 0.25s ease-in-out;
201
+ -o-transition: all 0.25s ease-in-out;
202
+ transition: all 0.25s ease-in-out;
203
+ }
204
+ .home-menu a.overview span {background:#1F6015;}
205
+ .home-menu a:hover span,
206
+ .home-menu a.on span {opacity:1;}
182
207
  #sub-header a.active{
183
208
  background:#666;
184
209
  background-image: -moz-linear-gradient(top, #555, #777);
@@ -203,6 +228,14 @@ h2 {
203
228
  margin:20px;
204
229
  }
205
230
 
231
+ h6 {
232
+ color:#474747;
233
+ font-size:14px;
234
+ font-weight:normal;
235
+ text-shadow:0 1px #fff;
236
+ margin:10px 5px;
237
+ }
238
+
206
239
  form {margin:5px 20px;}
207
240
  form div.form-section {
208
241
  width:400px;
@@ -324,15 +357,9 @@ div.flash {
324
357
  z-index:110;
325
358
  }
326
359
 
327
- div.table-wrapper {
328
- margin:10px 20px;
329
- }
360
+ div.table-wrapper { margin:10px 20px; }
330
361
 
331
- table.data-grid {
332
- font-size:12px;
333
- margin:10px 0;
334
- width:100%;
335
- }
362
+ table.data-grid { font-size:12px; margin:10px 0; width:100%; }
336
363
  table.data-grid thead tr,
337
364
  #states-list thead tr{
338
365
  background-color:#ddd;
@@ -542,3 +569,19 @@ p.hint {color:#474747;font-size:12px;line-height:20px;margin:8px 5px;width:300px
542
569
  margin:0 0 10px;
543
570
  text-transform:capitalize;
544
571
  }
572
+
573
+ table.overview {
574
+ width:100%;
575
+ }
576
+
577
+ table.overview td {
578
+ border-bottom:1px solid #eee;
579
+ font-size:12px;
580
+ padding:5px 8px 5px 5px;
581
+ text-shadow: 0 1px #fff;
582
+ vertical-align:middle;
583
+ }
584
+ table.overview tr:nth-child(even) {background:#fbfbfb;}
585
+ table.overview tr:last-child td {border-bottom:0px;}
586
+ table.overview td span.name {color:#474747;display:block;font-weight:bold; font-size:13px;padding-bottom:5px;}
587
+ table.overview td span.file {color:#999;display:block;font-style:italic;}
@@ -31,7 +31,8 @@ module Myreplicator
31
31
  # GET /exports/new.json
32
32
  def new
33
33
  @export = Export.new
34
-
34
+ @dbs = get_dbs
35
+ @tables = db_metadata
35
36
  respond_to do |format|
36
37
  format.html # new.html.erb
37
38
  format.json { render json: @export }
@@ -41,6 +42,8 @@ module Myreplicator
41
42
  # GET /exports/1/edit
42
43
  def edit
43
44
  @export = Export.find(params[:id])
45
+ @dbs = get_dbs
46
+ @tables = db_metadata
44
47
  @edit = true
45
48
  end
46
49
 
@@ -48,6 +51,7 @@ module Myreplicator
48
51
  # POST /exports.json
49
52
  def create
50
53
  @export = Export.new(params[:export])
54
+ @dbs = get_dbs
51
55
 
52
56
  respond_to do |format|
53
57
  if @export.save
@@ -64,6 +68,7 @@ module Myreplicator
64
68
  # PUT /exports/1.json
65
69
  def update
66
70
  @export = Export.find(params[:id])
71
+ @dbs = get_dbs
67
72
 
68
73
  respond_to do |format|
69
74
  if @export.update_attributes(params[:export])
@@ -101,6 +106,17 @@ module Myreplicator
101
106
  def sort_direction
102
107
  %w[asc desc].include?(params[:direction]) ? params[:direction] : "asc"
103
108
  end
109
+
110
+ def db_metadata
111
+ @db_metadata ||= Myreplicator::Export.available_tables
112
+ end
104
113
 
114
+ def get_dbs
115
+ return db_metadata.keys
116
+ end
117
+
118
+ def get_tables(db)
119
+ return db_metadata[db]
120
+ end
105
121
  end
106
122
  end
@@ -7,6 +7,8 @@ module Myreplicator
7
7
  @tab = 'home'
8
8
  @option = 'overview'
9
9
  @exports = Export.order('state DESC')
10
+ @logs = Log.where(:state => 'running').order("started_at DESC")
11
+ @now = Time.zone.now
10
12
  respond_to do |format|
11
13
  format.html # index.html.erb
12
14
  format.json { render json: @exports }
@@ -16,7 +18,8 @@ module Myreplicator
16
18
  def errors
17
19
  @tab = 'home'
18
20
  @option = 'errors'
19
- @exports = Export.where("error is not null").order('source_schema ASC')
21
+ @exports = Export.where("error is not null").order('source_schema ASC')
22
+ @logs = Log.where(:state => 'error').order("started_at DESC")
20
23
  end
21
24
 
22
25
  end
@@ -8,5 +8,25 @@ module Myreplicator
8
8
  link_to content_tag(:span, title), {:sort => column, :direction => direction}, {:class => css_class}
9
9
  end
10
10
 
11
+ def chronos(secs)
12
+ [[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].map{ |count, name|
13
+ if secs > 0
14
+ secs, n = secs.divmod(count)
15
+ "#{n.to_i} #{name}"
16
+ end
17
+ }.compact.reverse.join(', ')
18
+ end
19
+
20
+ def err_count
21
+ total = Log.where(:state => 'error').count
22
+ return total
23
+ end
24
+
25
+
26
+ def run_count
27
+ total = Log.where(:state => 'running').count
28
+ return total
29
+ end
30
+
11
31
  end
12
32
  end
@@ -28,8 +28,22 @@ module Myreplicator
28
28
  )
29
29
 
30
30
  attr_reader :filename
31
-
32
- def export
31
+
32
+ @queue = :myreplicator_export # Provided for Resque
33
+
34
+ ##
35
+ # Perfoms the export job, Provided for Resque
36
+ ##
37
+ def self.perform(export_id, *args)
38
+ options = args.extract_options!
39
+ export_obj = Export.find(export_id)
40
+ export_obj.export_table
41
+ end
42
+
43
+ ##
44
+ # Runs the export process using the required Exporter library
45
+ ##
46
+ def export_table
33
47
  exporter = MysqlExporter.new
34
48
  exporter.export_table self
35
49
  end
@@ -70,34 +84,90 @@ module Myreplicator
70
84
  puts "Connecting SFTP..."
71
85
  return connection_factory(:sftp)
72
86
  end
73
-
87
+
88
+ ##
89
+ # Connects to the server via ssh/sftp
90
+ ##
74
91
  def connection_factory type
92
+ config = Myreplicator.configs[self.source_schema]
93
+
75
94
  case type
76
95
  when :ssh
77
- if Myreplicator.configs[self.source_schema].has_key? "ssh_password"
78
- return Net::SSH.start(Myreplicator.configs[self.source_schema]["ssh_host"],
79
- Myreplicator.configs[self.source_schema]["ssh_user"],
80
- :password => Myreplicator.configs[self.source_schema]["ssh_password"])
81
-
82
- elsif(Myreplicator.configs[self.source_schema].has_key? "ssh_private_key")
83
- return Net::SSH.start(Myreplicator.configs[self.source_schema]["ssh_host"],
84
- Myreplicator.configs[self.source_schema]["ssh_user"],
85
- :keys => [Myreplicator.configs[self.source_schema]["ssh_private_key"]])
96
+ if config.has_key? "ssh_password"
97
+ return Net::SSH.start(config["ssh_host"], config["ssh_user"], :password => config["ssh_password"])
98
+
99
+ elsif(config.has_key? "ssh_private_key")
100
+ return Net::SSH.start(config["ssh_host"], config["ssh_user"], :keys => [config["ssh_private_key"]])
86
101
  end
87
102
  when :sftp
88
- if Myreplicator.configs[self.source_schema].has_key? "ssh_password"
89
- return Net::SFTP.start(Myreplicator.configs[self.source_schema]["ssh_host"],
90
- Myreplicator.configs[self.source_schema]["ssh_user"],
91
- :password => Myreplicator.configs[self.source_schema]["ssh_password"])
92
-
93
- elsif(Myreplicator.configs[self.source_schema].has_key? "ssh_private_key")
94
- return Net::SFTP.start(Myreplicator.configs[self.source_schema]["ssh_host"],
95
- Myreplicator.configs[self.source_schema]["ssh_user"],
96
- :keys => [Myreplicator.configs[self.source_schema]["ssh_private_key"]])
103
+ if config.has_key? "ssh_password"
104
+ return Net::SFTP.start(config["ssh_host"], config["ssh_user"], :password => config["ssh_password"])
105
+
106
+ elsif(config.has_key? "ssh_private_key")
107
+ return Net::SFTP.start(config["ssh_host"], config["ssh_user"], :keys => [config["ssh_private_key"]])
97
108
  end
98
109
  end
99
110
  end
100
111
 
112
+ ##
113
+ # Returns a hash of {DB_NAME => [TableName1,...], DB => ...}
114
+ ##
115
+ def self.available_tables
116
+ metadata = {}
117
+ available_dbs.each do |db|
118
+ tables = SourceDb.get_tables(db)
119
+ metadata[db] = tables
120
+ end
121
+ return metadata
122
+ end
123
+
124
+ ##
125
+ # List of all avaiable databases from database.yml file
126
+ # All Export/Load jobs can use these databases
127
+ ##
128
+ def self.available_dbs
129
+ dbs = ActiveRecord::Base.configurations.keys
130
+ dbs.delete("development")
131
+ dbs.delete("test")
132
+ return dbs
133
+ end
134
+
135
+ ##
136
+ # NOTE: Provided for Resque use
137
+ # Schedules all the exports in resque
138
+ # Requires Resque Scheduler
139
+ ##
140
+ def self.schedule_in_resque
141
+ exports = Export.find(:all)
142
+ exports.each do |export|
143
+ if export.active
144
+ export.schedule
145
+ else
146
+ Resque.remove_schedule(export.schedule_name)
147
+ end
148
+ end
149
+ Resque.reload_schedule! # Reload all schedules in Resque
150
+ end
151
+
152
+ ##
153
+ # Name used for the job in Resque
154
+ ##
155
+ def schedule_name
156
+ name = "#{source_schema}_#{destination_schema}_#{table_name}"
157
+ end
158
+
159
+ ##
160
+ # Schedules the export job in Resque
161
+ ##
162
+ def schedule
163
+ Resque.set_schedule(schedule_name, {
164
+ :cron => cron,
165
+ :class => "Myreplicator::Export",
166
+ :queue => "myreplicator_export",
167
+ :args => id
168
+ })
169
+ end
170
+
101
171
  ##
102
172
  # Inner Class that connects to the source database
103
173
  # Handles connecting to multiple databases
@@ -106,9 +176,24 @@ module Myreplicator
106
176
  class SourceDb < ActiveRecord::Base
107
177
 
108
178
  def self.connect db
109
- @@connected ||= true
110
179
  establish_connection(ActiveRecord::Base.configurations[db])
111
- Kernel.p ActiveRecord::Base.connected?
180
+ end
181
+
182
+ ##
183
+ # Returns tables as an Array
184
+ # releases the connection
185
+ ##
186
+ def self.get_tables(db)
187
+ tables = []
188
+ begin
189
+ self.connect(db)
190
+ tables = self.connection.tables
191
+ self.connection_pool.release_connection
192
+ rescue Mysql2::Error => e
193
+ puts "Connection to #{db} Failed!"
194
+ puts e.message
195
+ end
196
+ return tables
112
197
  end
113
198
 
114
199
  def self.exec_sql source_db,sql
@@ -0,0 +1,90 @@
1
+ module Myreplicator
2
+ class Log < ActiveRecord::Base
3
+ attr_accessible(:pid,
4
+ :job_type,
5
+ :name,
6
+ :file,
7
+ :state,
8
+ :thread_state,
9
+ :hostname,
10
+ :export_id,
11
+ :error,
12
+ :backtrace,
13
+ :guid,
14
+ :started_at,
15
+ :finished_at)
16
+
17
+ ##
18
+ # Creates a log object
19
+ # Stores the state and related information about the job
20
+ # File's names are supposed to be unique
21
+ ##
22
+ def self.run *args
23
+ options = args.extract_options!
24
+ options.reverse_merge!(:started_at => Time.now,
25
+ :pid => Process.pid,
26
+ :hostname => Socket.gethostname,
27
+ :guid => SecureRandom.hex(5),
28
+ :thread_state => Thread.current.status,
29
+ :state => "new")
30
+
31
+ log = Log.create options
32
+
33
+ unless log.running?
34
+ begin
35
+ log.state = "running"
36
+ log.save!
37
+
38
+ yield log
39
+
40
+ log.state = "completed"
41
+ rescue Exception => e
42
+ log.state = "error"
43
+ log.error = e.message
44
+ log.backtrace = e.backtrace
45
+
46
+ ensure
47
+ log.finished_at = Time.now
48
+ log.thread_state = Thread.current.status
49
+ log.save!
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ ##
56
+ # Kills the job if running
57
+ # Using PID
58
+ ##
59
+ def kill
60
+ begin
61
+ Process.kill('TERM', pid)
62
+ rescue Errno::ESRCH
63
+ puts "pid #{pid} does not exist!"
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Checks to see if the PID of the log is active or not
69
+ ##
70
+ def running?
71
+ logs = Log.where(:file => file, :job_type => job_type, :state => "running")
72
+
73
+ if logs.count > 0
74
+ logs.each do |log|
75
+ begin
76
+ Process.getpgid(log.pid)
77
+ puts "still running #{filepath}"
78
+ return true
79
+ rescue Errno::ESRCH
80
+ log.state = "error"
81
+ log.save!
82
+ end
83
+ end
84
+ end
85
+
86
+ return false
87
+ end
88
+
89
+ end
90
+ end