brbackup 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of brbackup might be problematic. Click here for more details.

@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Tung Nguyen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Brbackup
2
+ =======
3
+
4
+ Summary
5
+
6
+ Install
7
+ -------
8
+
9
+ <pre>
10
+ sudo gem install brbackup --source http://gemcutter.org
11
+ </pre>
12
+
13
+
14
+ Compatibility
15
+ -------------
16
+
17
+ Tested with Ruby 1.8.6.
18
+
19
+ Thanks
20
+ ------
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/gempackagetask'
4
+ require 'spec/rake/spectask'
5
+ require 'gemspec'
6
+
7
+ desc "Generate gemspec"
8
+ task :gemspec do
9
+ File.open("#{Dir.pwd}/#{GEM_NAME}.gemspec", 'w') do |f|
10
+ f.write(GEM_SPEC.to_ruby)
11
+ end
12
+ end
13
+
14
+ desc "Install gem"
15
+ task :install do
16
+ Rake::Task['gem'].invoke
17
+ `gem install pkg/#{GEM_NAME}*.gem`
18
+ `rm -Rf pkg`
19
+ end
20
+
21
+ desc "Package gem"
22
+ Rake::GemPackageTask.new(GEM_SPEC) do |pkg|
23
+ pkg.gem_spec = GEM_SPEC
24
+ end
25
+
26
+ desc "Run specs"
27
+ Spec::Rake::SpecTask.new do |t|
28
+ t.spec_opts = ["--format", "specdoc", "--colour"]
29
+ t.spec_files = FileList["spec/**/*_spec.rb"]
30
+ end
31
+
32
+ task :default => :spec do
33
+ end
34
+
35
+ # You can delete this after you use it
36
+ desc "Rename project"
37
+ task :rename do
38
+ name = ENV['NAME'] || File.basename(Dir.pwd)
39
+ class_name = name.split('_').collect{|x| x.capitalize}.join("")
40
+ begin
41
+ dir = Dir['**/gem_template*']
42
+ from = dir.pop
43
+ if from
44
+ rb = from.include?('.rb')
45
+ to = File.dirname(from) + "/#{name}#{'.rb' if rb}"
46
+ FileUtils.mv(from, to)
47
+ end
48
+ end while dir.length > 0
49
+ Dir["**/*"].each do |path|
50
+ next if path.include?('Rakefile')
51
+ if File.file?(path)
52
+ `sed -i "" 's/gem_template/#{name}/g' #{path}`
53
+ `sed -i "" 's/GemTemplate/#{class_name}/g' #{path}`
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/brbackup'
4
+
5
+ BR::Backups.run(ARGV)
@@ -0,0 +1,19 @@
1
+ GEM_NAME = 'brbackup'
2
+ GEM_FILES = FileList['**/*'] - FileList['coverage', 'coverage/**/*', 'pkg', 'pkg/**/*']
3
+ GEM_SPEC = Gem::Specification.new do |s|
4
+ # == CONFIGURE ==
5
+ s.author = "Tung Nguyen"
6
+ s.email = "tongueroo@gmail.com"
7
+ s.homepage = "http://github.com/tongueroo/#{GEM_NAME}"
8
+ s.summary = "brbackup summary"
9
+ # == CONFIGURE ==
10
+ s.executables = ["brbackup"]
11
+ s.add_dependency('builder', '>=2.1.2')
12
+ s.extra_rdoc_files = [ "README.markdown" ]
13
+ s.files = GEM_FILES.to_a
14
+ s.has_rdoc = false
15
+ s.name = GEM_NAME
16
+ s.platform = Gem::Platform::RUBY
17
+ s.require_path = "lib"
18
+ s.version = "0.1.0"
19
+ end
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'aws/s3'
5
+ require 'yaml'
6
+ require 'pp'
7
+ require 'optparse'
8
+ require 'open4'
9
+ require 'fileutils'
10
+ require 'logger'
11
+
12
+ module AWS::S3
13
+ class S3Object
14
+ def <=>(other)
15
+ DateTime.parse(self.about['last-modified']) <=> DateTime.parse(other.about['last-modified'])
16
+ end
17
+ end
18
+ end
19
+
20
+ # Usage:
21
+ # Simple:
22
+ # restores bleacherreport_production from production to staging db
23
+ # $ brbackup -f prod_br --clone bleacherreport
24
+ #
25
+ # More examples:
26
+ # Specifying more options
27
+ # $ brbackup -f prod_ss --clone cmservice --logger /tmp/brbackup.log -c .mysql.backups.yml
28
+ # $ brbackup -f prod_ss --clone cmservice --logger /tmp/brbackup.log -c .mysql.backups.yml
29
+ #
30
+ # Notes:
31
+ # Format:
32
+ # <bucket_name>/<env>.<db>/<filename>
33
+ # Example :
34
+ # ey-backup-fe5a050b997d.s3.amazonaws.com/...
35
+ # prod_br.bleacherreport_production/bleacherreport_production.2010-03-07T09-10-01.sql.gz
36
+ #
37
+ # The --clone option only needs the prefix of the database name because the amazon searches based
38
+ # on the prefix of <env>.<db>
39
+ # The filename of the dumpfile is then taken and then gsubed _production -> _staging
40
+ #
41
+ module BR
42
+ class DatabaseEngine
43
+ def self.register_as(name)
44
+ BR::Backups::ENGINES[name] = self
45
+ end
46
+
47
+ attr_accessor :logger
48
+
49
+ def initialize(backups)
50
+ @backups = backups
51
+ end
52
+
53
+ def dump_database(name)
54
+ raise "Implement #dump_database in #{self.class}"
55
+ end
56
+
57
+ def dbuser
58
+ @backups.config[:dbuser]
59
+ end
60
+
61
+ def dbpass
62
+ @backups.config[:dbpass]
63
+ end
64
+
65
+ def log(msg)
66
+ @logger.info(msg)
67
+ end
68
+ end
69
+
70
+ class Backups
71
+ ENGINES = {}
72
+
73
+ def self.run(args)
74
+ options = {}
75
+
76
+ # Build a parser for the command line arguments
77
+ opts = OptionParser.new do |opts|
78
+ opts.version = VERSION
79
+
80
+ opts.banner = "Usage: brbackup [-flag] [argument]"
81
+ opts.define_head "brbackup: clone db backups across environments"
82
+ opts.separator '*'*80
83
+
84
+ opts.on("-f", "--from ENVIRONMENT", "EY Cloud environment name want to clone from : prod_br, beta, alpha, etc") do |env|
85
+ options[:env] = env
86
+ end
87
+
88
+ opts.on("-l", "--list-backup DATABASE", "List mysql backups for DATABASE") do |db|
89
+ options[:db] = (db || 'all')
90
+ options[:command] = :list
91
+ end
92
+
93
+ # opts.on("-n", "--names DB1,DB2,DB3", "Only restore these databases") do |n|
94
+ # options[:databases] = n.split(',')
95
+ # end
96
+
97
+ opts.on("-c", "--config CONFIG", "Use config file.") do |config|
98
+ options[:config] = config
99
+ end
100
+
101
+ opts.on("-d", "--download BACKUP_INDEX", "download the backup specified by index. Run brbackup -l to get the index.") do |index|
102
+ options[:command] = :download
103
+ options[:index] = index
104
+ end
105
+
106
+ opts.on("--clone DB_NAME", "Clones production database to staging") do |db_name|
107
+ options[:command] = :clone
108
+ options[:db_name] = db_name
109
+ end
110
+
111
+ opts.on("--logger /path/to/logger", "Path to log path, default is stdout") do |logger|
112
+ options[:logger_path] = logger
113
+ end
114
+
115
+ # opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index WARNING! will overwrite the current db with the backup. Run brbackup -l to get the index.") do |index|
116
+ # options[:command] = :restore
117
+ # options[:index] = index
118
+ # end
119
+
120
+ end
121
+
122
+ opts.parse!(args)
123
+
124
+ options[:engine] ||= 'mysql'
125
+ options[:config] ||= "/etc/.#{options[:engine]}.backups.yml"
126
+
127
+ brb = new(options)
128
+
129
+ case options[:command]
130
+ when :list
131
+ brb.list options[:db], true
132
+ when :download
133
+ brb.download(options[:index])
134
+ # when :restore
135
+ # brb.restore(options[:index])
136
+ when :clone
137
+ brb.clone(options[:db_name])
138
+ end
139
+ rescue SystemExit
140
+ exit 1
141
+ rescue Exception => e
142
+ $stderr.puts "An unknown exception was raised"
143
+ $stderr.puts e.inspect
144
+ $stderr.puts e.backtrace
145
+ $stderr.puts
146
+ raise
147
+ end
148
+
149
+ def initialize(options)
150
+ if options[:logger_path]
151
+ @logger = Logger.new(options[:logger_path])
152
+ else
153
+ @logger = Logger.new($stdout)
154
+ end
155
+
156
+ engine_klass = ENGINES[options[:engine]] || raise("Invalid database engine: #{options[:engine].inspect}")
157
+ @engine = engine_klass.new(self)
158
+ @engine.logger = @logger
159
+
160
+ load_config(options[:config])
161
+
162
+ AWS::S3::Base.establish_connection!(
163
+ :access_key_id => config[:aws_secret_id],
164
+ :secret_access_key => config[:aws_secret_key]
165
+ )
166
+ @databases = options[:databases] || config[:databases]
167
+ @keep = config[:keep]
168
+ @bucket = "ey-backup-#{Digest::SHA1.hexdigest(config[:aws_secret_id])[0..11]}"
169
+ @tmpname = "#{Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')}.sql.gz"
170
+ @env = options[:env] || config[:env]
171
+ FileUtils.mkdir_p '/mnt/backups'
172
+ FileUtils.mkdir_p '/mnt/tmp'
173
+ begin
174
+ AWS::S3::Bucket.find(@bucket)
175
+ rescue AWS::S3::NoSuchBucket
176
+ AWS::S3::Bucket.create(@bucket)
177
+ end
178
+
179
+ FileUtils.mkdir_p self.backup_dir
180
+ end
181
+ attr_reader :config
182
+
183
+ def load_config(filename)
184
+ if File.exist?(filename)
185
+ @config = YAML::load(File.read(filename))
186
+ else
187
+ log "You need to have a backup file at #{filename}"
188
+ $stderr.puts "You need to have a backup file at #{filename}"
189
+ exit 1
190
+ end
191
+ end
192
+
193
+ def new_backup
194
+ @databases.each do |db|
195
+ backup_database(db)
196
+ end
197
+ end
198
+
199
+ def backup_database(database)
200
+ File.open("#{self.backup_dir}/#{database}.#{@tmpname}", "w") do |f|
201
+ log "doing database: #{database}"
202
+ @engine.dump_database(database, f)
203
+ end
204
+
205
+ File.open("#{self.backup_dir}/#{database}.#{@tmpname}") do |f|
206
+ path = "#{@env}.#{database}/#{database}.#{@tmpname}"
207
+ AWS::S3::S3Object.store(path, f, @bucket, :access => :private)
208
+ log "successful backup: #{database}.#{@tmpname}"
209
+ end
210
+ end
211
+
212
+ def download(index)
213
+ idx, db = index.split(":")
214
+ raise Error, "You didn't specify a database name: e.g. 1:rails_production" unless db
215
+
216
+ if obj = list(db)[idx.to_i]
217
+ filename = normalize_name(obj)
218
+ log "downloading: #{filename}"
219
+ File.open(filename, 'wb') do |f|
220
+ print "."
221
+ obj.value {|chunk| f.write chunk }
222
+ end
223
+ log ""
224
+ log "finished"
225
+ [db, filename]
226
+ else
227
+ raise BackupNotFound, "No backup found for database #{db.inspect}: requested index: #{idx}"
228
+ end
229
+ end
230
+
231
+ def restore(index)
232
+ db, filename = download(index)
233
+ File.open(filename) do |f|
234
+ @engine.restore_database(db, f)
235
+ end
236
+ end
237
+
238
+ def clone(db_name)
239
+ index = most_recent_index(db_name)
240
+ db, filename = download(index)
241
+ log "db #{db.inspect}"
242
+ log "filename #{filename.inspect}"
243
+ staging_name = filename.split('.')[0].gsub('_production', '_staging')
244
+ File.open(filename) do |f|
245
+ @engine.clone_database(staging_name, f)
246
+ end
247
+ end
248
+
249
+ def cleanup
250
+ begin
251
+ list('all',false)[0...-(@keep*@databases.size)].each do |o|
252
+ log "deleting: #{o.key}"
253
+ o.delete
254
+ end
255
+ rescue AWS::S3::S3Exception, AWS::S3::Error
256
+ nil # see bucket_minder cleanup note regarding S3 consistency
257
+ end
258
+ end
259
+
260
+ def normalize_name(obj)
261
+ obj.key.gsub(/^.*?\//, '')
262
+ end
263
+
264
+ def find_obj(name)
265
+ AWS::S3::S3Object.find name, @bucket
266
+ end
267
+
268
+ def list(database='all', printer = false)
269
+ puts "Listing database backups for #{database}" if printer
270
+ backups = []
271
+ if database == 'all'
272
+ @databases.each do |db|
273
+ backups << AWS::S3::Bucket.objects(@bucket, :prefix => "#{@env}.#{db}")
274
+ end
275
+ backups = backups.flatten.sort
276
+ else
277
+ backups = AWS::S3::Bucket.objects(@bucket, :prefix => "#{@env}.#{database}").sort
278
+ end
279
+ if printer
280
+ puts "#{backups.size} backup(s) found"
281
+ backups.each_with_index do |b,i|
282
+ puts "#{i}:#{database} #{normalize_name(b)}"
283
+ end
284
+ end
285
+ backups
286
+ end
287
+
288
+ # dirty way to get most recent index
289
+ def most_recent_index(db)
290
+ index = list(db).size - 1
291
+ "#{index}:#{db}"
292
+ end
293
+
294
+ protected
295
+ def backup_dir
296
+ "/mnt/tmp"
297
+ end
298
+
299
+ def log(msg)
300
+ @logger.info(msg)
301
+ end
302
+ end
303
+
304
+ class MysqlDatabase < DatabaseEngine
305
+ register_as 'mysql'
306
+
307
+ def dump_database(name, io)
308
+ single_transaction = db_has_myisam?(name) ? '' : '--single-transaction'
309
+ Open4.spawn ["mysqldump -u#{dbuser} #{password_option} #{single_transaction} #{name} | gzip -c"], :stdout => io
310
+ end
311
+
312
+ def db_has_myisam?(name)
313
+ query = "SELECT 1 FROM information_schema.tables WHERE table_schema='#{name}' AND engine='MyISAM' LIMIT 1;"
314
+ %x{mysql -u #{dbuser} #{password_option} -N -e"#{query}"}.strip == '1'
315
+ end
316
+
317
+ def restore_database(name, io)
318
+ log "mock restoring database..."
319
+ # Open4.spawn ["gzip -dc | mysql -u#{dbuser} #{password_option} #{name}"], :stdin => io
320
+ end
321
+
322
+ def clone_database(staging_name, io)
323
+ log "dropping #{staging_name} database"
324
+ cmd = "mysql -u#{dbuser} #{password_option} -e 'drop database #{staging_name}'"
325
+ Open4.popen4 cmd do |pid, stdin, stdout, stderr|
326
+ log stdout.read
327
+ end
328
+ log "creating #{staging_name} database"
329
+ Open4.popen4 "mysql -u#{dbuser} #{password_option} -e 'create database #{staging_name}'" do |pid, stdin, stdout, stderr|
330
+ log stdout.read
331
+ end
332
+ log "loading new dump..."
333
+ Open4.spawn ["gzip -dc | mysql -u#{dbuser} #{password_option} #{staging_name}"], :stdin => io
334
+ log "new dump loaded."
335
+ end
336
+
337
+ def password_option
338
+ dbpass.nil? || dbpass.blank? ? "" : "-p'#{dbpass}'"
339
+ end
340
+
341
+ end
342
+ end
@@ -0,0 +1,6 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")
2
+
3
+ describe Brbackup do
4
+
5
+
6
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,18 @@
1
+ $testing = true
2
+ SPEC = File.dirname(__FILE__)
3
+ $:.unshift File.expand_path("#{SPEC}/../lib")
4
+
5
+ require 'brbackup'
6
+ require 'pp'
7
+ require 'rubygems'
8
+
9
+ Spec::Runner.configure do |config|
10
+ end
11
+
12
+ # For use with rspec textmate bundle
13
+ def debug(object)
14
+ puts "<pre>"
15
+ puts object.pretty_inspect.gsub('<', '&lt;').gsub('>', '&gt;')
16
+ puts "</pre>"
17
+ end
18
+
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brbackup
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Tung Nguyen
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-21 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: builder
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 1
30
+ - 2
31
+ version: 2.1.2
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description:
35
+ email: tongueroo@gmail.com
36
+ executables:
37
+ - brbackup
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - README.markdown
42
+ files:
43
+ - bin/brbackup
44
+ - gemspec.rb
45
+ - lib/brbackup.rb
46
+ - MIT-LICENSE
47
+ - Rakefile
48
+ - README.markdown
49
+ - spec/brbackup/brbackup.rb
50
+ - spec/spec.opts
51
+ - spec/spec_helper.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/tongueroo/brbackup
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.6
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: brbackup summary
82
+ test_files: []
83
+