ey_cloud_server 1.4.5 → 1.4.26

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,48 @@
1
+ module EY
2
+ module Backup
3
+ class Database
4
+ def initialize(engine, base_path, keep, backend, environment, name)
5
+ if name.nil? || name.empty?
6
+ raise ArgumentError, "database name is blank"
7
+ end
8
+ @engine = engine
9
+ @base_path = base_path
10
+ @keep = keep
11
+ @backend = backend
12
+ @environment = environment
13
+ @name = name
14
+ end
15
+ attr_reader :name, :engine, :environment, :keep, :base_path
16
+
17
+ def bucket_minder
18
+ @backend.bucket_minder
19
+ end
20
+
21
+ def backups
22
+ s3_objects = bucket_minder.list("#{@environment}.#{@name}").select { |o| o[:name] =~ @engine.suffix }
23
+ s3_objects.map {|o| BackupSet.new(self, o[:name], nil, o[:keys]) }.sort
24
+ end
25
+
26
+ def dump
27
+ FileUtils.mkdir_p(@base_path)
28
+
29
+ basename = generate_basename
30
+ file = @engine.dump(@name, basename)
31
+
32
+ BackupSet.from(self, basename, file)
33
+ end
34
+
35
+ def start_upload(filenames)
36
+ @backend.start_upload(filenames, @environment, @name)
37
+ end
38
+
39
+ def generate_basename
40
+ "#{@base_path}/#{@name}.#{timestamp}"
41
+ end
42
+
43
+ def timestamp
44
+ Time.now.strftime("%Y-%m-%dT%H-%M-%S")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,47 +1,31 @@
1
1
  module EY
2
2
  module Backup
3
3
  class Dumper < Base
4
- attr_accessor :database
4
+ include Logging
5
5
 
6
- def_delegators EY::Backup, :engine, :minder, :base_path
6
+ attr_reader :database
7
7
 
8
- def self.run(options = {})
9
- if database = options[:db]
10
- new(database, options).run
11
- else
12
- options[:databases].each do |database|
13
- new(database, options).run
14
- end
8
+ def self.run(databases, split_size)
9
+ databases.each do |database|
10
+ new(database).run(split_size)
15
11
  end
16
12
  end
17
13
 
18
- def initialize(database, options = {})
19
- super(options)
20
- @database = database
14
+ def initialize(database)
15
+ @database = database
21
16
  end
22
17
 
23
- def run
24
- info "doing database: #{database}"
18
+ def run(split_size)
19
+ info "Doing database: #{@database.name}"
25
20
 
26
- FileUtils.mkdir_p('/mnt/tmp')
21
+ backup_set = @database.dump
22
+ backup_set.split!(split_size)
23
+ backup_set.upload!
27
24
 
28
- backup = engine.dump(database, base_name)
29
-
30
- backup.upload!(database)
31
-
32
- info "Successful backup: #{database}.#{backup.normalized_name}"
33
- BackupSet.cleanup(database, EY::Backup.config.keep)
34
- end
35
-
36
-
37
- def base_name
38
- "#{base_path}/#{database}.#{timestamp}"
39
- end
40
-
41
- def timestamp
42
- @timestamp ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')
25
+ info "Successful backup: #{@database.name}.#{backup_set.normalized_name}"
26
+ backup_set.cleanup
27
+ backup_set.rm!
43
28
  end
44
-
45
29
  end
46
30
  end
47
31
  end
@@ -3,7 +3,7 @@ module EY
3
3
  class Engine < Base
4
4
  include Spawner
5
5
 
6
- attr_accessor :username, :password
6
+ attr_reader :username, :password, :host
7
7
 
8
8
  def self.label
9
9
  @label
@@ -26,25 +26,15 @@ module EY
26
26
  EY::Backup.logger.fatal("Unknown database engine: #{label}")
27
27
  end
28
28
 
29
- def initialize(username, password)
30
- @username, @password = username, password
31
- end
32
-
33
- def post_dump_process(backup)
34
- processors.inject(backup) do |backup, processor|
35
- processor.dump(backup)
36
- end
37
- end
38
-
39
- def pre_load_process(backup)
40
- processors.reverse.inject(backup) do |backup, processor|
41
- processor.load(backup)
42
- end
29
+ def initialize(username, password, host, key_id)
30
+ @username, @password, @host = username, password, host
31
+ @key_id = key_id
43
32
  end
33
+ attr_reader :key_id
44
34
 
45
35
  def gpg?
46
- not EY::Backup.config.key_id.nil? &&
47
- EY::Backup.config.key_id.blank?
36
+ not @key_id.nil? &&
37
+ @key_id.blank?
48
38
  end
49
39
  end
50
40
  end
@@ -3,13 +3,13 @@ module EY
3
3
  class MysqlEngine < Engine
4
4
  register 'mysql'
5
5
 
6
- def dump(database, basename)
6
+ def dump(database_name, basename)
7
7
  file = basename + '.sql'
8
8
 
9
- command = "mysqldump #{username_option} #{password_option} #{single_transaction_option(database)} #{database}"
9
+ command = "mysqldump #{username_option} #{host_option} #{password_option} #{single_transaction_option(database_name)} #{routines_option} #{database_name}"
10
10
 
11
11
  if gpg?
12
- command << " | " << GPGEncryptor.command
12
+ command << " | " << GPGEncryptor.command_for(key_id)
13
13
  file << GPGEncryptor.extension
14
14
  else
15
15
  command << " | " << GZipper.gzip
@@ -20,44 +20,52 @@ module EY
20
20
 
21
21
  run(command)
22
22
 
23
- Splitter.dump(BackupSet.from(database, file))
23
+ file
24
24
  end
25
25
 
26
- def load(database, backup)
27
- backup = Splitter.load(backup)
28
-
29
- command = "cat #{backup.files.first}"
26
+ def load(database_name, file)
27
+ command = "cat #{file}"
30
28
 
31
29
  if gpg?
32
- command << " | " << GPGEncryptor.load_command
30
+ raise "Cannot load a GPG backup"
33
31
  else
34
32
  command << " | " << GZipper.gunzip
35
33
  end
36
34
 
37
- command << " | mysql #{username_option} #{password_option} #{database}"
35
+ command << " | mysql #{username_option} #{host_option} #{password_option} #{database_name}"
38
36
 
39
37
  run(command)
40
38
  end
41
39
 
40
+ def routines_option
41
+ "--routines"
42
+ end
43
+
42
44
  def password_option
43
45
  "-p'#{password}'" unless password.nil? || password.empty?
44
46
  end
45
47
 
48
+ def host_option
49
+ "-h#{host}"
50
+ end
51
+
46
52
  def username_option
47
53
  "-u#{username}"
48
54
  end
49
55
 
50
- def single_transaction_option(database)
51
- '--single-transaction' unless db_has_myisam?(database)
56
+ def single_transaction_option(database_name)
57
+ '--single-transaction' unless db_has_myisam?(database_name)
52
58
  end
53
59
 
54
- def db_has_myisam?(database)
55
- query, stdout = "SELECT 1 FROM information_schema.tables WHERE table_schema='#{database}' AND engine='MyISAM' LIMIT 1;", StringIO.new
56
- spawn(%Q{mysql #{username_option} #{password_option} -N -e"#{query}"}, stdout)
60
+ def db_has_myisam?(database_name)
61
+ query, stdout = "SELECT 1 FROM information_schema.tables WHERE table_schema='#{database_name}' AND engine='MyISAM' LIMIT 1;", StringIO.new
62
+ spawn(%Q{mysql #{username_option} #{password_option} #{host_option} -N -e"#{query}"}, stdout)
57
63
  stdout.string.strip == '1'
58
64
  end
59
65
 
60
-
66
+ def suffix
67
+ /sql\.(gz|gpz)$/
68
+ end
61
69
  end
62
70
  end
63
71
  end
@@ -3,13 +3,13 @@ module EY
3
3
  class Postgresql < Engine
4
4
  register 'postgresql'
5
5
 
6
- def dump(database, basename)
6
+ def dump(database_name, basename)
7
7
  file = basename + '.pgz'
8
8
 
9
- command = "PGPASSWORD='#{password}' pg_dump -h localhost --format=c --no-owner --no-privileges -U#{username} #{database}"
9
+ command = "PGPASSWORD='#{password}' pg_dump -h #{host} --format=c --no-owner --no-privileges -U#{username} #{database_name}"
10
10
 
11
11
  if gpg?
12
- command << " | " << GPGEncryptor.command
12
+ command << " | " << GPGEncryptor.command_for(key_id)
13
13
  file << GPGEncryptor.extension
14
14
  end
15
15
 
@@ -17,40 +17,46 @@ module EY
17
17
 
18
18
  run(command)
19
19
 
20
- Splitter.dump(BackupSet.from(database, file))
20
+ file
21
21
  end
22
22
 
23
- def load(database, backup)
24
- backup = Splitter.load(backup)
25
-
26
- cycle_database(database) if database_exists?(database)
23
+ def load(database_name, file)
24
+ if database_exists?(database_name)
25
+ cycle_database(database_name)
26
+ else
27
+ create_database(database_name)
28
+ end
27
29
 
28
- command = "cat #{backup.files.first}"
30
+ command = "cat #{file}"
29
31
 
30
32
  if gpg?
31
- command << " | " << GPGEncryptor.load_command
33
+ raise "Cannot load a GPG backup"
32
34
  end
33
35
 
34
- command << " | PGPASSWORD='#{password}' pg_restore -h localhost --format=c --ignore-version -U#{username} -d #{database}"
36
+ command << " | PGPASSWORD='#{password}' pg_restore -h #{host} --format=c --ignore-version -U#{username} -d #{database_name}"
35
37
 
36
38
  run(command)
37
39
  end
38
40
 
39
- def database_exists?(database)
40
- spawn("PGPASSWORD='#{password}' psql -l | grep '#{database}'").success?
41
+ def database_exists?(database_name)
42
+ runs?("PGPASSWORD='#{password}' psql -l -h #{host} | grep '#{database_name}'")
43
+ end
44
+
45
+ def drop_database(database_name)
46
+ spawn("PGPASSWORD='#{password}' dropdb -h #{host} -U#{username} #{database_name}")
41
47
  end
42
48
 
43
- def drop_database(database)
44
- spawn("PGPASSWORD='#{password}' dropdb -h localhost -U#{username} #{database}")
49
+ def create_database(database_name)
50
+ spawn("PGPASSWORD='#{password}' createdb -U#{username} -h #{host} #{database_name}")
45
51
  end
46
52
 
47
- def create_database(database)
48
- spawn("PGPASSWORD='#{password}' createdb -U#{username} -h localhost #{database}")
53
+ def cycle_database(database_name)
54
+ drop_database(database_name)
55
+ create_database(database_name)
49
56
  end
50
57
 
51
- def cycle_database(database)
52
- drop_database(database)
53
- create_database(database)
58
+ def suffix
59
+ /pgz(\.gpz)?$/
54
60
  end
55
61
  end
56
62
  end
@@ -3,52 +3,32 @@ module EY
3
3
  class Loader < Base
4
4
  include Logging
5
5
 
6
- attr_accessor :database, :index
7
-
8
- def_delegators EY::Backup, :engine
9
-
10
- def self.run(options = {})
11
- index, database = options[:index].split(':')
12
- new(database, index, options).run
6
+ def self.run(database, index)
7
+ new(database, index).run
13
8
  end
14
9
 
15
- def self.download(options = {})
16
- index, database = options[:index].split(':')
17
- new(database, index, options).download_raw
10
+ def self.download(database, index)
11
+ new(database, index).download
18
12
  end
19
13
 
20
- def initialize(database, index, options = {})
21
- super(options)
14
+ def initialize(database, index)
22
15
  @database, @index = database, index
23
16
  end
24
17
 
25
18
  def run
19
+ info "Restoring #{@database.name}"
26
20
  backup = download
27
- info "Restoring #{database}"
28
- engine.load(database, backup)
29
- backup.rm!
21
+ backup.load!
22
+ backup.remove_joined_file!
30
23
  end
31
24
 
32
25
  def download
33
- BackupSet.download(database, index)
34
- end
35
-
36
- def download_raw
37
- FileUtils.mkdir_p('/mnt/tmp')
38
-
39
- download
40
- end
41
-
42
- def base_name
43
- "#{base_path}/#{database}.#{timestamp}"
44
- end
45
-
46
- def timestamp
47
- @timestamp ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')
48
- end
26
+ FileUtils.mkdir_p(EY::Backup.tmp_dir)
49
27
 
50
- def base_path
51
- "/mnt/tmp"
28
+ backup = BackupSet.download(@database, @index)
29
+ backup.join!
30
+ backup.remove_split_files!
31
+ backup
52
32
  end
53
33
  end
54
34
  end
@@ -1,30 +1,13 @@
1
1
  module EY
2
2
  module Backup
3
3
  module GPGEncryptor
4
- extend Spawner
5
- extend self
6
-
7
- def dump(backup)
8
- backup.files = backup.files.map {|file| encrypt(file) }
9
- backup
10
- end
11
-
12
- def command
13
- "gpg --trusted-key #{EY::Backup.config.key_id} --encrypt --recipient #{EY::Backup.config.key_id}"
4
+ def self.command_for(key_id)
5
+ "gpg -v --trusted-key #{key_id} --encrypt --recipient #{key_id}"
14
6
  end
15
7
 
16
- def extension
8
+ def self.extension
17
9
  ".gpz"
18
10
  end
19
-
20
- def load(backup)
21
- backup.files = backup.files.map {|file| decrypt(file) }
22
- backup
23
- end
24
-
25
- def decrypt(file)
26
- fatal "Restoring encrypted backups is not supported."
27
- end
28
11
  end
29
12
  end
30
13
  end
@@ -17,35 +17,6 @@ module EY
17
17
  def extension
18
18
  ".gz"
19
19
  end
20
-
21
- def load(backup)
22
- output_files = backup.files.map do |input|
23
- if input =~ /\.gz$/
24
- output = input.sub(/\.gz$/, '')
25
-
26
- File.open(output, 'w') do |o|
27
- File.open(input, 'r') do |i|
28
- gz = Zlib::GzipReader.new(i)
29
-
30
- while chunk = gz.read(CHUNK_SIZE)
31
- o << chunk
32
- end
33
-
34
- gz.close
35
- end
36
- end
37
-
38
- FileUtils.rm(input)
39
-
40
- output
41
- else
42
- input
43
- end
44
- end
45
-
46
- backup.files = output_files
47
- backup
48
- end
49
20
  end
50
21
  end
51
22
  end