ey_cloud_server 1.3.1 → 1.4.5.pre
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 +253 -0
- data/bin/clear_binlogs_from_slave +7 -0
- data/bin/ey-snapshots +6 -1
- data/bin/eybackup +3 -2
- data/lib/ey-flex.rb +11 -3
- data/lib/ey-flex/bucket_minder.rb +91 -16
- data/lib/ey-flex/snapshot_minder.rb +39 -31
- data/lib/ey-flex/version.rb +1 -1
- data/lib/ey_backup.rb +96 -0
- data/lib/ey_backup/backup_set.rb +128 -0
- data/lib/ey_backup/base.rb +18 -0
- data/lib/ey_backup/cli.rb +89 -0
- data/lib/ey_backup/dumper.rb +47 -0
- data/lib/ey_backup/engine.rb +51 -0
- data/lib/ey_backup/engines/mysql_engine.rb +63 -0
- data/lib/ey_backup/engines/postgresql_engine.rb +57 -0
- data/lib/ey_backup/loader.rb +55 -0
- data/lib/ey_backup/logger.rb +36 -0
- data/lib/ey_backup/processors/gpg_encryptor.rb +30 -0
- data/lib/ey_backup/processors/gzipper.rb +53 -0
- data/lib/ey_backup/processors/splitter.rb +96 -0
- data/lib/ey_backup/spawner.rb +54 -0
- data/lib/ey_cloud_server/mysql_start.rb +21 -14
- metadata +71 -39
- data/lib/ey-flex/backups.rb +0 -233
- data/lib/ey-flex/mysql_database.rb +0 -23
- data/lib/ey-flex/postgresql_database.rb +0 -14
@@ -0,0 +1,47 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class Dumper < Base
|
4
|
+
attr_accessor :database
|
5
|
+
|
6
|
+
def_delegators EY::Backup, :engine, :minder, :base_path
|
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
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(database, options = {})
|
19
|
+
super(options)
|
20
|
+
@database = database
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
info "doing database: #{database}"
|
25
|
+
|
26
|
+
FileUtils.mkdir_p('/mnt/tmp')
|
27
|
+
|
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(/:/, '-')
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class Engine < Base
|
4
|
+
include Spawner
|
5
|
+
|
6
|
+
attr_accessor :username, :password
|
7
|
+
|
8
|
+
def self.label
|
9
|
+
@label
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.register(label)
|
13
|
+
@label = label
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.descendants
|
17
|
+
@descendants ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.inherited(descendant)
|
21
|
+
descendants << descendant
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.lookup(label)
|
25
|
+
descendants.detect {|d| d.label == label } ||
|
26
|
+
EY::Backup.logger.fatal("Unknown database engine: #{label}")
|
27
|
+
end
|
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
|
43
|
+
end
|
44
|
+
|
45
|
+
def gpg?
|
46
|
+
not EY::Backup.config.key_id.nil? &&
|
47
|
+
EY::Backup.config.key_id.blank?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class MysqlEngine < Engine
|
4
|
+
register 'mysql'
|
5
|
+
|
6
|
+
def dump(database, basename)
|
7
|
+
file = basename + '.sql'
|
8
|
+
|
9
|
+
command = "mysqldump #{username_option} #{password_option} #{single_transaction_option(database)} #{database}"
|
10
|
+
|
11
|
+
if gpg?
|
12
|
+
command << " | " << GPGEncryptor.command
|
13
|
+
file << GPGEncryptor.extension
|
14
|
+
else
|
15
|
+
command << " | " << GZipper.gzip
|
16
|
+
file << GZipper.extension
|
17
|
+
end
|
18
|
+
|
19
|
+
command << " > #{file}"
|
20
|
+
|
21
|
+
run(command)
|
22
|
+
|
23
|
+
Splitter.dump(BackupSet.from(database, file))
|
24
|
+
end
|
25
|
+
|
26
|
+
def load(database, backup)
|
27
|
+
backup = Splitter.load(backup)
|
28
|
+
|
29
|
+
command = "cat #{backup.files.first}"
|
30
|
+
|
31
|
+
if gpg?
|
32
|
+
command << " | " << GPGEncryptor.load_command
|
33
|
+
else
|
34
|
+
command << " | " << GZipper.gunzip
|
35
|
+
end
|
36
|
+
|
37
|
+
command << " | mysql #{username_option} #{password_option} #{database}"
|
38
|
+
|
39
|
+
run(command)
|
40
|
+
end
|
41
|
+
|
42
|
+
def password_option
|
43
|
+
"-p'#{password}'" unless password.nil? || password.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def username_option
|
47
|
+
"-u#{username}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def single_transaction_option(database)
|
51
|
+
'--single-transaction' unless db_has_myisam?(database)
|
52
|
+
end
|
53
|
+
|
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)
|
57
|
+
stdout.string.strip == '1'
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class Postgresql < Engine
|
4
|
+
register 'postgresql'
|
5
|
+
|
6
|
+
def dump(database, basename)
|
7
|
+
file = basename + '.pgz'
|
8
|
+
|
9
|
+
command = "PGPASSWORD='#{password}' pg_dump -h localhost --format=c --no-owner --no-privileges -U#{username} #{database}"
|
10
|
+
|
11
|
+
if gpg?
|
12
|
+
command << " | " << GPGEncryptor.command
|
13
|
+
file << GPGEncryptor.extension
|
14
|
+
end
|
15
|
+
|
16
|
+
command << " > #{file}"
|
17
|
+
|
18
|
+
run(command)
|
19
|
+
|
20
|
+
Splitter.dump(BackupSet.from(database, file))
|
21
|
+
end
|
22
|
+
|
23
|
+
def load(database, backup)
|
24
|
+
backup = Splitter.load(backup)
|
25
|
+
|
26
|
+
cycle_database(database) if database_exists?(database)
|
27
|
+
|
28
|
+
command = "cat #{backup.files.first}"
|
29
|
+
|
30
|
+
if gpg?
|
31
|
+
command << " | " << GPGEncryptor.load_command
|
32
|
+
end
|
33
|
+
|
34
|
+
command << " | PGPASSWORD='#{password}' pg_restore -h localhost --format=c --ignore-version -U#{username} -d #{database}"
|
35
|
+
|
36
|
+
run(command)
|
37
|
+
end
|
38
|
+
|
39
|
+
def database_exists?(database)
|
40
|
+
spawn("PGPASSWORD='#{password}' psql -l | grep '#{database}'").success?
|
41
|
+
end
|
42
|
+
|
43
|
+
def drop_database(database)
|
44
|
+
spawn("PGPASSWORD='#{password}' dropdb -h localhost -U#{username} #{database}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_database(database)
|
48
|
+
spawn("PGPASSWORD='#{password}' createdb -U#{username} -h localhost #{database}")
|
49
|
+
end
|
50
|
+
|
51
|
+
def cycle_database(database)
|
52
|
+
drop_database(database)
|
53
|
+
create_database(database)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class Loader < Base
|
4
|
+
include Logging
|
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
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.download(options = {})
|
16
|
+
index, database = options[:index].split(':')
|
17
|
+
new(database, index, options).download_raw
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(database, index, options = {})
|
21
|
+
super(options)
|
22
|
+
@database, @index = database, index
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
backup = download
|
27
|
+
info "Restoring #{database}"
|
28
|
+
engine.load(database, backup)
|
29
|
+
backup.rm!
|
30
|
+
end
|
31
|
+
|
32
|
+
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
|
49
|
+
|
50
|
+
def base_path
|
51
|
+
"/mnt/tmp"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
class Logger
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_accessor :stdout, :stderr
|
7
|
+
|
8
|
+
alias_method :fatal, :abort
|
9
|
+
public :fatal
|
10
|
+
|
11
|
+
def_delegator :stderr, :puts, :error
|
12
|
+
def_delegator :stderr, :puts, :warn
|
13
|
+
def_delegator :stdout, :puts, :info
|
14
|
+
def_delegator :stdout, :puts, :puts
|
15
|
+
|
16
|
+
def self.quiet
|
17
|
+
new(StringIO.new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(stdout = $stdout, stderr = $stderr)
|
21
|
+
@stdout, @stderr = stdout, stderr
|
22
|
+
end
|
23
|
+
|
24
|
+
def say(msg, newline = true)
|
25
|
+
newline ? info(msg) : stdout.print(msg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Logging
|
30
|
+
extend Forwardable
|
31
|
+
|
32
|
+
def_delegator EY::Backup, :logger
|
33
|
+
def_delegators :logger, :fatal, :error, :warn, :info, :puts, :say
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
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}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def extension
|
17
|
+
".gpz"
|
18
|
+
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
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
module GZipper
|
4
|
+
extend Spawner
|
5
|
+
extend self
|
6
|
+
|
7
|
+
CHUNK_SIZE = 1024 * 64
|
8
|
+
|
9
|
+
def gzip
|
10
|
+
"gzip -c"
|
11
|
+
end
|
12
|
+
|
13
|
+
def gunzip
|
14
|
+
"gunzip -c"
|
15
|
+
end
|
16
|
+
|
17
|
+
def extension
|
18
|
+
".gz"
|
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
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module EY
|
2
|
+
module Backup
|
3
|
+
module Splitter
|
4
|
+
extend Spawner
|
5
|
+
extend Logging
|
6
|
+
extend self
|
7
|
+
|
8
|
+
CHUNK_SIZE = 1024 * 64
|
9
|
+
MAX_FILE_SIZE = (4.5*1024*1024*1024).to_i #4.5GB
|
10
|
+
|
11
|
+
def load(backup)
|
12
|
+
if backup.files.size > 1
|
13
|
+
backup.files = join(backup.files)
|
14
|
+
end
|
15
|
+
|
16
|
+
backup
|
17
|
+
end
|
18
|
+
|
19
|
+
def dump(command, files)
|
20
|
+
if files.size != 1
|
21
|
+
fatal "Can't split multiple files."
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def dump(backup)
|
27
|
+
output_files = backup.files.map do |file|
|
28
|
+
if File.size(file) >= split_size
|
29
|
+
split(file)
|
30
|
+
else
|
31
|
+
file
|
32
|
+
end
|
33
|
+
end.flatten
|
34
|
+
|
35
|
+
backup.files = output_files
|
36
|
+
backup
|
37
|
+
end
|
38
|
+
|
39
|
+
def join(input_files)
|
40
|
+
filename = input_files.first.sub(/\.part\d+$/, '')
|
41
|
+
|
42
|
+
File.open(filename, 'w') do |output|
|
43
|
+
sort(input_files).each do |input|
|
44
|
+
File.open(input, 'r') do |i|
|
45
|
+
while chunk = i.read(CHUNK_SIZE)
|
46
|
+
output << chunk
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
FileUtils.rm(input)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
[filename]
|
55
|
+
end
|
56
|
+
|
57
|
+
def sort(input_files)
|
58
|
+
input_files.sort {|a,b| part_number(a) <=> part_number(b) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def part_number(file)
|
62
|
+
file[/\.part(\d+)/, 1].to_i
|
63
|
+
end
|
64
|
+
|
65
|
+
def split(file)
|
66
|
+
total_size, part = 0, 0
|
67
|
+
files = []
|
68
|
+
File.open(file, 'r') do |i|
|
69
|
+
until total_size == File.size(file)
|
70
|
+
part += 1
|
71
|
+
part_size = 0
|
72
|
+
filename = "#{file}.part#{part}"
|
73
|
+
File.open(filename, 'w') do |o|
|
74
|
+
while part_size < split_size && (chunk = i.read([split_size - part_size, CHUNK_SIZE].min))
|
75
|
+
part_size += chunk.size
|
76
|
+
o << chunk
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
total_size += part_size
|
81
|
+
|
82
|
+
files << filename
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
FileUtils.rm(file)
|
87
|
+
|
88
|
+
files
|
89
|
+
end
|
90
|
+
|
91
|
+
def split_size
|
92
|
+
EY::Backup.config.split_size || MAX_FILE_SIZE
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|