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.
- 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
|