ey_cloud_server 1.4.5 → 1.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/bin/binary_log_purge +10 -2
  2. data/bin/ey-snapshots +7 -1
  3. data/bin/eybackup +13 -1
  4. data/lib/ey-flex.rb +18 -7
  5. data/lib/ey-flex/big-brother.rb +2 -2
  6. data/lib/ey-flex/bucket_minder.rb +46 -160
  7. data/lib/ey-flex/ec2.rb +17 -0
  8. data/lib/ey-flex/snapshot_minder.rb +93 -171
  9. data/lib/ey_backup.rb +84 -48
  10. data/lib/ey_backup/backend.rb +34 -0
  11. data/lib/ey_backup/backup_set.rb +70 -63
  12. data/lib/ey_backup/base.rb +0 -5
  13. data/lib/ey_backup/cli.rb +26 -6
  14. data/lib/ey_backup/database.rb +48 -0
  15. data/lib/ey_backup/dumper.rb +15 -31
  16. data/lib/ey_backup/engine.rb +7 -17
  17. data/lib/ey_backup/engines/mysql_engine.rb +24 -16
  18. data/lib/ey_backup/engines/postgresql_engine.rb +26 -20
  19. data/lib/ey_backup/loader.rb +13 -33
  20. data/lib/ey_backup/processors/gpg_encryptor.rb +3 -20
  21. data/lib/ey_backup/processors/gzipper.rb +0 -29
  22. data/lib/ey_backup/processors/splitter.rb +22 -34
  23. data/lib/ey_backup/spawner.rb +7 -13
  24. data/lib/ey_cloud_server.rb +1 -1
  25. data/lib/{ey-flex → ey_cloud_server}/version.rb +1 -1
  26. data/spec/big-brother_spec.rb +12 -0
  27. data/spec/bucket_minder_spec.rb +113 -0
  28. data/spec/config-example.yml +11 -0
  29. data/spec/ey_api_spec.rb +63 -0
  30. data/spec/ey_backup/backend_spec.rb +12 -0
  31. data/spec/ey_backup/backup_spec.rb +54 -0
  32. data/spec/ey_backup/cli_spec.rb +35 -0
  33. data/spec/ey_backup/mysql_backups_spec.rb +208 -0
  34. data/spec/ey_backup/postgres_backups_spec.rb +106 -0
  35. data/spec/ey_backup/spec_helper.rb +5 -0
  36. data/spec/fakefs_hax.rb +50 -0
  37. data/spec/gpg.public +0 -0
  38. data/spec/gpg.sekrit +0 -0
  39. data/spec/helpers.rb +270 -0
  40. data/spec/snapshot_minder_spec.rb +68 -0
  41. data/spec/spec_helper.rb +31 -0
  42. metadata +286 -53
data/lib/ey_backup.rb CHANGED
@@ -6,74 +6,108 @@ require 'open4'
6
6
  require 'zlib'
7
7
  require 'ostruct'
8
8
  require 'fileutils'
9
+ require 'fog'
9
10
 
10
11
  module EY
11
12
  module Backup
12
- extend self
13
- attr_accessor :logger, :bucket_minder, :engine, :config
13
+ class << self
14
+ attr_accessor :logger
15
+ attr_accessor :tmp_dir
16
+ end
14
17
 
15
- def run(argv = ARGV)
18
+ def self.run(argv = ARGV)
16
19
  options = CLI.run(argv)
17
- setup(options)
18
- dispatch(options)
20
+ Runner.new(options).run
19
21
  end
20
22
 
21
- def new_backup(options = {})
22
- Dumper.run(options)
23
- end
23
+ class Runner
24
+ def initialize(options)
25
+ @options = options
26
+ end
24
27
 
25
- def list(options = {})
26
- if options[:db].nil? || options[:db].empty?
27
- BackupSet.list_all
28
- else
29
- #list a single database's backups
30
- BackupSet.list(options[:db])
28
+ def run
29
+ setup
30
+ dispatch
31
31
  end
32
- end
33
32
 
34
- def restore(options = {})
35
- Loader.run(options)
36
- end
33
+ # COMMANDS
37
34
 
38
- def download(options = {})
39
- Loader.download(options)
40
- end
35
+ def new_backup
36
+ Dumper.run(databases, @options[:split_size])
37
+ end
41
38
 
42
- def setup(options)
43
- setup_config(options)
44
- setup_logger(options)
45
- setup_minder(options)
46
- setup_engine(options)
47
- end
39
+ def list
40
+ BackupSet.list(databases)
41
+ end
48
42
 
49
- def setup_config(options)
50
- @config = OpenStruct.new(options)
51
- end
43
+ def restore
44
+ databases.each do |database|
45
+ Loader.run(database, @options[:index])
46
+ end
47
+ end
52
48
 
53
- def setup_logger(options)
54
- if options[:quiet]
55
- @logger ||= Logger.quiet
56
- else
57
- @logger ||= Logger.new
49
+ def download
50
+ databases.each do |database|
51
+ Loader.download(database, @options[:index])
52
+ end
58
53
  end
59
- end
60
54
 
61
- def setup_minder(options)
62
- @bucket = "ey-backup-#{Digest::SHA1.hexdigest(options[:aws_secret_id])[0..11]}"
63
- @bucket_minder = EY::BucketMinder.new(options.merge(:bucket => @bucket))
64
- end
55
+ # END COMMANDS
65
56
 
66
- def setup_engine(options)
67
- @engine = Engine.lookup(options[:engine]).new(options[:dbuser], options[:dbpass])
68
- end
57
+ def databases
58
+ @databases ||= database_names.map do |name|
59
+ Database.new(@engine, EY::Backup.tmp_dir, @options[:keep], @backend, @options[:environment], name)
60
+ end
61
+ end
69
62
 
70
- def dispatch(options)
71
- send(options[:command], options)
72
- end
63
+ def database_names
64
+ if @options[:db].nil? || @options[:db].empty?
65
+ @options[:databases]
66
+ else
67
+ [@options[:db]]
68
+ end
69
+ end
70
+
71
+ def setup
72
+ setup_logger
73
+ setup_tmp_dir
74
+ setup_backend
75
+ setup_engine
76
+ end
77
+
78
+ def setup_logger
79
+ if @options[:quiet]
80
+ EY::Backup.logger ||= Logger.quiet
81
+ else
82
+ EY::Backup.logger ||= Logger.new
83
+ end
84
+ end
85
+
86
+ def setup_tmp_dir
87
+ if @options[:tmp_dir]
88
+ EY::Backup.tmp_dir = @options[:tmp_dir]
89
+ else
90
+ EY::Backup.tmp_dir = "/mnt/tmp"
91
+ end
92
+ end
93
+
94
+ def setup_backend
95
+ @backend = Backend.new(@options[:aws_secret_id], @options[:aws_secret_key], @options[:region], @options[:backup_bucket])
96
+ end
73
97
 
74
- def base_path
75
- "/mnt/tmp"
98
+ def setup_engine
99
+ engine_class = Engine.lookup(@options[:engine])
100
+ if ! @options.key?(:dbhost) or @options[:dbhost] == nil or @options[:dbhost] == ""
101
+ @options[:dbhost] = 'localhost'
102
+ end
103
+ @engine = engine_class.new(@options[:dbuser], @options[:dbpass], @options[:dbhost], @options[:key_id])
104
+ end
105
+
106
+ def dispatch
107
+ send(@options[:command])
108
+ end
76
109
  end
110
+
77
111
  end
78
112
  end
79
113
 
@@ -84,9 +118,11 @@ require lib_dir + '/logger'
84
118
  require lib_dir + '/base'
85
119
  require lib_dir + '/backup_set'
86
120
  require lib_dir + '/cli'
121
+ require lib_dir + '/database'
87
122
  require lib_dir + '/dumper'
88
123
  require lib_dir + '/engine'
89
124
  require lib_dir + '/loader'
125
+ require lib_dir + '/backend'
90
126
 
91
127
  require lib_dir + '/engines/mysql_engine'
92
128
  require lib_dir + '/engines/postgresql_engine'
@@ -0,0 +1,34 @@
1
+ require 'fog'
2
+
3
+ module EY
4
+ module Backup
5
+ class Backend
6
+ include EY::Backup::Logging
7
+ extend EY::Backup::Logging
8
+
9
+ def initialize(secret_id, secret_key, region, bucket_name)
10
+ @bucket_minder = EY::BucketMinder.new(secret_id, secret_key, bucket_name, region)
11
+ @s3 = Fog::Storage.new(:provider => 'AWS', :aws_access_key_id => secret_id, :aws_secret_access_key => secret_key, :region => region)
12
+ end
13
+ attr_reader :bucket_minder
14
+
15
+ def start_upload(filenames, environment_name, database_name)
16
+ filenames.each do |filename|
17
+ begin
18
+ object_name = "#{environment_name}.#{database_name}/#{File.basename(filename)}"
19
+ info "Starting upload: #{filename}"
20
+ @s3.put_object(@bucket_minder.bucket_name, object_name, File.open(filename,'r'))
21
+ info "Successful upload: #{filename}"
22
+ rescue => e
23
+ retries ||= 5
24
+ retries -= 1
25
+ raise e if retries == 0
26
+ warn "retrying upload of #{filename}. Got: #{e.inspect}"
27
+ retry
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -1,65 +1,56 @@
1
1
  module EY
2
2
  module Backup
3
- class BackupSet < Struct.new(:name, :keys)
3
+ class BackupSet
4
4
  include EY::Backup::Logging
5
5
  extend EY::Backup::Logging
6
6
 
7
- attr_accessor :files
8
-
9
7
  MULTIPART_EXTENSION = /\.part\d+/
10
8
 
11
- def self.parse(objects)
12
- objects.map {|o| new(o[:name], o[:keys]) }
9
+ def self.from(database, basename, file)
10
+ new(database, basename, file, nil)
13
11
  end
14
12
 
15
- def self.from(database, file)
16
- backup = new(File.basename(file), [])
17
- backup.files = [file]
18
- backup
13
+ def initialize(database, basename, filename, keys)
14
+ @database = database
15
+ @basename = basename
16
+ @filename = filename
17
+ @keys = keys
19
18
  end
19
+ attr_reader :basename
20
20
 
21
- def self.cleanup(database, keep)
22
- say "Cleanup for #{database}"
23
- backups_for(database)[0...(-keep)].each do |backup|
24
- say "deleting: #{backup.name}"
25
- backup.delete!
26
- end
21
+ def database_name
22
+ @database.name
27
23
  end
28
24
 
29
- def self.list_all
30
- list('all', backups)
25
+ def cleanup
26
+ say "Cleanup for #{database_name}"
27
+ @database.backups[0...(-@database.keep)].each do |backup|
28
+ say "Deleting: #{backup.basename}"
29
+ backup.delete!
30
+ end
31
31
  end
32
32
 
33
- def self.backups
34
- EY::Backup.config.databases.map {|db| backups_for(db) }.flatten
35
- end
33
+ def self.list(databases)
34
+ names = databases.map do |db|
35
+ db.name
36
+ end
37
+ info "Listing database backups for #{names.join(', ')}"
36
38
 
37
- def self.list(database, backups = backups_for(database))
38
- info "Listing database backups for #{database}"
39
+ backups = databases.map{ |db| db.backups }.flatten
39
40
 
40
41
  info "#{backups.size} backup(s) found"
41
42
 
42
- backups.each_with_index do |db, i|
43
- info "#{i}:#{database} #{db.normalized_name}"
43
+ backups.each_with_index do |backup_set, i|
44
+ info "#{i}:#{backup_set.database_name} #{backup_set.normalized_name}"
44
45
  end
45
46
 
46
47
  backups
47
48
  end
48
49
 
49
- def self.backups_for(database_name)
50
- s3_objects = EY::Backup.bucket_minder.list(:prefix => "#{EY::Backup.config.environment}.#{database_name}", :print => false)
51
- parse(s3_objects).sort
52
- end
53
-
54
- def self.upload(database, file)
55
- EY::Backup.bucket_minder.name = "#{EY::Backup.config.environment}.#{database}/#{File.basename(file)}"
56
- EY::Backup.bucket_minder.upload_object(file, false)
57
- end
58
-
59
50
  def self.download(database, index)
60
- fatal "You didn't specify a database name: e.g. 1:rails_production" if database.empty? || index.empty?
51
+ fatal "You didn't specify a database name: e.g. 1:rails_production" if database.nil? || index.empty?
61
52
 
62
- backup = backups_for(database)[index.to_i]
53
+ backup = database.backups[index.to_i]
63
54
 
64
55
  fatal "No backup found for database #{database}: requested index: #{index}" unless backup
65
56
 
@@ -68,60 +59,76 @@ module EY
68
59
  backup
69
60
  end
70
61
 
71
- def initialize(*)
72
- super
73
-
74
- @files = []
75
- end
76
-
77
62
  def parts
78
- [keys.size, files.size].max
63
+ @keys.size
79
64
  end
80
65
 
81
66
  def normalized_name
82
- normalize(name)
67
+ normalize(@basename)
83
68
  end
84
69
 
85
- def upload!(database)
86
- files.each do |file|
87
- EY::Backup.bucket_minder.name = "#{EY::Backup.config.environment}.#{database}/#{File.basename(file)}"
88
- EY::Backup.bucket_minder.upload_object(file, false)
89
- info "successful upload: #{file}"
90
- end
70
+ def load!
71
+ @database.engine.load(@database.name, @filename)
72
+ end
73
+
74
+ def upload!
75
+ @database.start_upload(@remote_filenames)
91
76
  end
92
77
 
93
78
  def download!
94
- keys.each do |key|
95
- info "Downloading #{key}"
96
- filename = File.join(EY::Backup.base_path, normalize(key))
97
- File.open(filename, 'wb') do |f|
98
- EY::Backup.bucket_minder.stream(key) do |chunk|
99
- f << chunk
79
+ @remote_filenames = []
80
+ @keys.each do |key|
81
+ info "Downloading #{key} to #{@database.base_path}"
82
+ remote_filename = File.join(@database.base_path, normalize(key))
83
+ File.open(remote_filename, 'wb') do |f|
84
+ @database.bucket_minder.stream(key) do |chunk, remaining_size, total_size|
85
+ f.write(chunk)
100
86
  end
101
87
  end
102
88
 
103
- files << filename
89
+ @remote_filenames << remote_filename
104
90
  end
105
91
  end
106
92
 
107
93
  def delete!
108
- keys.each do |key|
109
- EY::Backup.bucket_minder.remove_object(key)
94
+ @keys.each do |key|
95
+ @database.bucket_minder.remove_object(key)
110
96
  end
111
97
  end
112
98
 
113
- def rm!
114
- files.each do |file|
115
- FileUtils.rm(file)
99
+ def remove_split_files!
100
+ (@remote_filenames - [@filename]).each do |filename|
101
+ FileUtils.rm(filename)
116
102
  end
117
103
  end
118
104
 
105
+ def remove_joined_file!
106
+ FileUtils.rm(@filename)
107
+ end
108
+
109
+ def rm!
110
+ remove_split_files!
111
+ remove_joined_file!
112
+ end
113
+
114
+ def split!(split_size)
115
+ @remote_filenames = Splitter.dump(@filename, split_size)
116
+ end
117
+
118
+ def join!
119
+ @filename = Splitter.load(@remote_filenames)
120
+ end
121
+
119
122
  def normalize(o)
120
123
  o.gsub(/^.*?\//, '')
121
124
  end
122
125
 
126
+ def timestamp
127
+ @basename[/(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/, 1]
128
+ end
129
+
123
130
  def <=>(o)
124
- self.name <=> o.name
131
+ self.timestamp <=> o.timestamp
125
132
  end
126
133
  end
127
134
  end
@@ -5,11 +5,6 @@ module EY
5
5
 
6
6
  def_delegators :logger, :fatal, :error, :warn, :info, :debug, :say
7
7
 
8
- def initialize(options = {})
9
- @environment = options[:env]
10
- @engine_name = options[:engine]
11
- end
12
-
13
8
  def logger
14
9
  EY::Backup.logger
15
10
  end
data/lib/ey_backup/cli.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module EY
2
2
  module Backup
3
- module CLI
3
+ module CLI #rename to option parser and config loader
4
4
  extend self
5
5
 
6
6
  def run(argv)
@@ -27,11 +27,14 @@ module EY
27
27
  opts.version = EY::CloudServer::VERSION
28
28
 
29
29
  opts.banner = "Usage: eybackup [-flag] [argument]"
30
- opts.define_head "eybackup: backing up your shit since way back when..."
30
+ opts.define_head "eybackup: manage dump (mysqldump/pg_dump) style backups of your database."
31
31
  opts.separator '*'*80
32
32
 
33
33
  opts.on("-l", "--list-backup DATABASE", "List mysql backups for DATABASE") do |db|
34
- options[:db] = (db || 'all')
34
+ if db == "all"
35
+ db = nil
36
+ end
37
+ options[:db] = db
35
38
  options[:command] = :list
36
39
  end
37
40
 
@@ -43,18 +46,26 @@ module EY
43
46
  options[:config] = config
44
47
  end
45
48
 
46
- opts.on("-d", "--download BACKUP_INDEX", "download the backup specified by index. Run eybackup -l to get the index.") do |index|
49
+ opts.on("-t", "--tmp_dir TMPDIR", "Use the given directory for temporary storage.") do |tmp_dir|
50
+ options[:tmp_dir] = tmp_dir
51
+ end
52
+
53
+ opts.on("-d", "--download BACKUP_INDEX", "download the backup specified by index. Run eybackup -l to get the index.") do |index_and_db|
47
54
  options[:command] = :download
55
+ db, index = split_index(index_and_db)
48
56
  options[:index] = index
57
+ options[:db] = db
49
58
  end
50
59
 
51
60
  opts.on("-e", "--engine DATABASE_ENGINE", "The database engine. ex: mysql, postgres.") do |engine|
52
61
  options[:engine] = engine
53
62
  end
54
63
 
55
- opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index WARNING! will overwrite the current db with the backup. Run eybackup -l to get the index.") do |index|
64
+ opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index WARNING! will overwrite the current db with the backup. Run eybackup -l to get the index.") do |index_and_db|
56
65
  options[:command] = :restore
66
+ db, index = split_index(index_and_db)
57
67
  options[:index] = index
68
+ options[:db] = db
58
69
  end
59
70
 
60
71
  opts.on("-k", "--key KEYID", "Public key ID to use for the backup operation") do |key_id|
@@ -75,9 +86,18 @@ module EY
75
86
  options
76
87
  end
77
88
 
89
+ def split_index(index)
90
+ index.split(':').reverse
91
+ end
92
+
78
93
  def config_for(filename)
79
94
  if File.exist?(filename)
80
- config = YAML::load(File.read(filename))
95
+ # Make the loaded config have symbols rather than strings
96
+ config_orig = YAML::load(File.read(filename))
97
+ config = {}
98
+ config_orig.each do |key, val|
99
+ config[key.to_sym] = val
100
+ end
81
101
  config[:environment] ||= config[:env]
82
102
  config
83
103
  else