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.
- data/bin/binary_log_purge +10 -2
- data/bin/ey-snapshots +7 -1
- data/bin/eybackup +13 -1
- data/lib/ey-flex.rb +18 -7
- data/lib/ey-flex/big-brother.rb +2 -2
- data/lib/ey-flex/bucket_minder.rb +46 -160
- data/lib/ey-flex/ec2.rb +17 -0
- data/lib/ey-flex/snapshot_minder.rb +93 -171
- data/lib/ey_backup.rb +84 -48
- data/lib/ey_backup/backend.rb +34 -0
- data/lib/ey_backup/backup_set.rb +70 -63
- data/lib/ey_backup/base.rb +0 -5
- data/lib/ey_backup/cli.rb +26 -6
- data/lib/ey_backup/database.rb +48 -0
- data/lib/ey_backup/dumper.rb +15 -31
- data/lib/ey_backup/engine.rb +7 -17
- data/lib/ey_backup/engines/mysql_engine.rb +24 -16
- data/lib/ey_backup/engines/postgresql_engine.rb +26 -20
- data/lib/ey_backup/loader.rb +13 -33
- data/lib/ey_backup/processors/gpg_encryptor.rb +3 -20
- data/lib/ey_backup/processors/gzipper.rb +0 -29
- data/lib/ey_backup/processors/splitter.rb +22 -34
- data/lib/ey_backup/spawner.rb +7 -13
- data/lib/ey_cloud_server.rb +1 -1
- data/lib/{ey-flex → ey_cloud_server}/version.rb +1 -1
- data/spec/big-brother_spec.rb +12 -0
- data/spec/bucket_minder_spec.rb +113 -0
- data/spec/config-example.yml +11 -0
- data/spec/ey_api_spec.rb +63 -0
- data/spec/ey_backup/backend_spec.rb +12 -0
- data/spec/ey_backup/backup_spec.rb +54 -0
- data/spec/ey_backup/cli_spec.rb +35 -0
- data/spec/ey_backup/mysql_backups_spec.rb +208 -0
- data/spec/ey_backup/postgres_backups_spec.rb +106 -0
- data/spec/ey_backup/spec_helper.rb +5 -0
- data/spec/fakefs_hax.rb +50 -0
- data/spec/gpg.public +0 -0
- data/spec/gpg.sekrit +0 -0
- data/spec/helpers.rb +270 -0
- data/spec/snapshot_minder_spec.rb +68 -0
- data/spec/spec_helper.rb +31 -0
- 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
|
data/lib/ey_backup/dumper.rb
CHANGED
@@ -1,47 +1,31 @@
|
|
1
1
|
module EY
|
2
2
|
module Backup
|
3
3
|
class Dumper < Base
|
4
|
-
|
4
|
+
include Logging
|
5
5
|
|
6
|
-
|
6
|
+
attr_reader :database
|
7
7
|
|
8
|
-
def self.run(
|
9
|
-
|
10
|
-
new(database
|
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
|
19
|
-
|
20
|
-
@database = database
|
14
|
+
def initialize(database)
|
15
|
+
@database = database
|
21
16
|
end
|
22
17
|
|
23
|
-
def run
|
24
|
-
info "
|
18
|
+
def run(split_size)
|
19
|
+
info "Doing database: #{@database.name}"
|
25
20
|
|
26
|
-
|
21
|
+
backup_set = @database.dump
|
22
|
+
backup_set.split!(split_size)
|
23
|
+
backup_set.upload!
|
27
24
|
|
28
|
-
backup
|
29
|
-
|
30
|
-
|
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
|
data/lib/ey_backup/engine.rb
CHANGED
@@ -3,7 +3,7 @@ module EY
|
|
3
3
|
class Engine < Base
|
4
4
|
include Spawner
|
5
5
|
|
6
|
-
|
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
|
-
|
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
|
47
|
-
|
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(
|
6
|
+
def dump(database_name, basename)
|
7
7
|
file = basename + '.sql'
|
8
8
|
|
9
|
-
command = "mysqldump #{username_option} #{password_option} #{single_transaction_option(
|
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.
|
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
|
-
|
23
|
+
file
|
24
24
|
end
|
25
25
|
|
26
|
-
def load(
|
27
|
-
|
28
|
-
|
29
|
-
command = "cat #{backup.files.first}"
|
26
|
+
def load(database_name, file)
|
27
|
+
command = "cat #{file}"
|
30
28
|
|
31
29
|
if gpg?
|
32
|
-
|
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} #{
|
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(
|
51
|
-
'--single-transaction' unless db_has_myisam?(
|
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?(
|
55
|
-
query, stdout = "SELECT 1 FROM information_schema.tables WHERE table_schema='#{
|
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(
|
6
|
+
def dump(database_name, basename)
|
7
7
|
file = basename + '.pgz'
|
8
8
|
|
9
|
-
command = "PGPASSWORD='#{password}' pg_dump -h
|
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.
|
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
|
-
|
20
|
+
file
|
21
21
|
end
|
22
22
|
|
23
|
-
def load(
|
24
|
-
|
25
|
-
|
26
|
-
|
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 #{
|
30
|
+
command = "cat #{file}"
|
29
31
|
|
30
32
|
if gpg?
|
31
|
-
|
33
|
+
raise "Cannot load a GPG backup"
|
32
34
|
end
|
33
35
|
|
34
|
-
command << " | PGPASSWORD='#{password}' pg_restore -h
|
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?(
|
40
|
-
|
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
|
44
|
-
spawn("PGPASSWORD='#{password}'
|
49
|
+
def create_database(database_name)
|
50
|
+
spawn("PGPASSWORD='#{password}' createdb -U#{username} -h #{host} #{database_name}")
|
45
51
|
end
|
46
52
|
|
47
|
-
def
|
48
|
-
|
53
|
+
def cycle_database(database_name)
|
54
|
+
drop_database(database_name)
|
55
|
+
create_database(database_name)
|
49
56
|
end
|
50
57
|
|
51
|
-
def
|
52
|
-
|
53
|
-
create_database(database)
|
58
|
+
def suffix
|
59
|
+
/pgz(\.gpz)?$/
|
54
60
|
end
|
55
61
|
end
|
56
62
|
end
|
data/lib/ey_backup/loader.rb
CHANGED
@@ -3,52 +3,32 @@ module EY
|
|
3
3
|
class Loader < Base
|
4
4
|
include Logging
|
5
5
|
|
6
|
-
|
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(
|
16
|
-
|
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
|
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
|
-
|
28
|
-
|
29
|
-
backup.rm!
|
21
|
+
backup.load!
|
22
|
+
backup.remove_joined_file!
|
30
23
|
end
|
31
24
|
|
32
25
|
def download
|
33
|
-
|
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
|
-
|
51
|
-
|
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
|
-
|
5
|
-
|
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
|