sqlup 0.0.1
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/History.txt +5 -0
- data/Manifest.txt +38 -0
- data/README.txt +122 -0
- data/Rakefile +18 -0
- data/bin/sqlup +107 -0
- data/bin/sqlup_control +25 -0
- data/config/environment.rb +0 -0
- data/lib/mysql_backup/entity/files/innodb.rb +77 -0
- data/lib/mysql_backup/entity/files/myisam.rb +62 -0
- data/lib/mysql_backup/entity/files.rb +95 -0
- data/lib/mysql_backup/entity/identifier.rb +179 -0
- data/lib/mysql_backup/entity/logs.rb +44 -0
- data/lib/mysql_backup/entity/mysqldump.rb +57 -0
- data/lib/mysql_backup/entity.rb +38 -0
- data/lib/mysql_backup/librarian/backup.rb +134 -0
- data/lib/mysql_backup/librarian/backup_collection.rb +63 -0
- data/lib/mysql_backup/librarian.rb +176 -0
- data/lib/mysql_backup/server.rb +237 -0
- data/lib/mysql_backup/storage/s3.rb +110 -0
- data/lib/mysql_backup/storage.rb +13 -0
- data/lib/mysql_backup/utilities/factory_create_method.rb +51 -0
- data/lib/mysql_backup.rb +8 -0
- data/lib/sqlup.rb +2 -0
- data/test/unit/mysql_backup/entity/files/files_test.rb +45 -0
- data/test/unit/mysql_backup/entity/files/innodb_test.rb +50 -0
- data/test/unit/mysql_backup/entity/files/myisam_test.rb +42 -0
- data/test/unit/mysql_backup/entity/identifier_test.rb +51 -0
- data/test/unit/mysql_backup/entity/logs_test.rb +13 -0
- data/test/unit/mysql_backup/entity/mysqldump_test.rb +64 -0
- data/test/unit/mysql_backup/librarian/backup_collection_test.rb +52 -0
- data/test/unit/mysql_backup/librarian/backup_test.rb +25 -0
- data/test/unit/mysql_backup/librarian/librarian_test.rb +103 -0
- data/test/unit/mysql_backup/server_test.rb +63 -0
- data/test/unit/mysql_backup/storage/s3_test.rb +69 -0
- data/test/unit/mysql_backup/storage/test_helper.rb +1 -0
- data/test/unit/mysql_backup/test_helper.rb +1 -0
- data/test/unit/mysql_backup/utilities/test_helper.rb +1 -0
- data/test/unit/test_helper.rb +10 -0
- metadata +116 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'named_arguments'
|
3
|
+
require 'mysql_backup/utilities/factory_create_method'
|
4
|
+
|
5
|
+
module MysqlBackup; end
|
6
|
+
class MysqlBackup::Entity; end;
|
7
|
+
|
8
|
+
# There are four different kinds of things you store for MySQL backups.
|
9
|
+
#
|
10
|
+
# - Completed logs.
|
11
|
+
# - The current log (the log file that MySQL is writing to)
|
12
|
+
# - mysqldump files (output from the mysqldump command)
|
13
|
+
# - binary files (tar | gzip | split of the files in the mysql data directory)
|
14
|
+
#
|
15
|
+
# They're stored using the following name schemes:
|
16
|
+
#
|
17
|
+
# :log:type_complete:thelog.000005
|
18
|
+
# A completed log file.
|
19
|
+
#
|
20
|
+
# :log:type_current:log_file_thelog.0000000006:log_position_0000000311:n_parts_0000000001:part_number_0000000000
|
21
|
+
# A current log file. The position is 311 (that's where mysql will write the next statement).
|
22
|
+
# There's only one part, and this is it. (Part numbers start with 0.)
|
23
|
+
# In this release, log files can only have one part - no attempt is made to split them
|
24
|
+
# into chunks small enough to fit in S3. Don't let your log files grow larger than 5G.
|
25
|
+
# :full:type_mysqldump:log_file_thelog.0000000006:log_position_0000000382:n_parts_0000000001:part_number_0000000000
|
26
|
+
# A full mysqldump file. The current log file is thelog.0000000006, and the position in
|
27
|
+
# that log is 382. Since we don't flush logs, anything written after 382 might not be
|
28
|
+
# complete.
|
29
|
+
# :full:type_binary:log_file_thelog.0000000006:log_position_0000000311:n_parts_0000000001:part_number_0000000000
|
30
|
+
# A full copy of the MySQL data files, created by tarring up all the data files,
|
31
|
+
# then passing them through gzip and split to make sure they'll fit in S3 objects that
|
32
|
+
# can only hold 5G.
|
33
|
+
class MysqlBackup::Entity::Identifier
|
34
|
+
include NamedArguments
|
35
|
+
include FactoryCreateMethod
|
36
|
+
|
37
|
+
# category is one of
|
38
|
+
# :full => a full backup of the mysql files
|
39
|
+
# :log_current => the mysql binary log file that's being written to
|
40
|
+
# :log_complete => mysql binary log files that are complete
|
41
|
+
attr_accessor :category
|
42
|
+
|
43
|
+
# type depends on the category.
|
44
|
+
#
|
45
|
+
# for category +full+, type is one of
|
46
|
+
# :binary, :mysqldump
|
47
|
+
attr_accessor :type
|
48
|
+
|
49
|
+
# The name of the log file
|
50
|
+
attr_accessor :log_file
|
51
|
+
|
52
|
+
# The numeric position in the log file
|
53
|
+
attr_accessor :log_position
|
54
|
+
|
55
|
+
# For multipart storage units, the part number
|
56
|
+
attr_accessor :part_number
|
57
|
+
|
58
|
+
# For multipart storage units, the total number of parts
|
59
|
+
attr_accessor :n_parts
|
60
|
+
|
61
|
+
# Time the object was created
|
62
|
+
attr_accessor :timestamp
|
63
|
+
|
64
|
+
def initialize args
|
65
|
+
super
|
66
|
+
|
67
|
+
part_number ||= 0
|
68
|
+
part_number = sprintf "%06d", part_number.to_i
|
69
|
+
|
70
|
+
n_parts ||= 1
|
71
|
+
n_parts = sprintf "%06d", n_parts.to_i
|
72
|
+
|
73
|
+
throw :bad if log_file == 'foo'
|
74
|
+
end
|
75
|
+
|
76
|
+
def merge new_values_hash
|
77
|
+
to_hash.merge new_values_hash
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_hash
|
81
|
+
result = {}
|
82
|
+
[:category, :type, :log_file, :log_position, :n_parts, :part_number].each do |i|
|
83
|
+
result[i] = send i
|
84
|
+
end
|
85
|
+
result
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
pos = n_digits log_position
|
90
|
+
np = n_digits n_parts
|
91
|
+
pn = n_digits part_number
|
92
|
+
return "#{category}:type_#{@type}:log_file_#{log_file}:log_position_#{pos}:n_parts_#{np}:part_number_#{pn}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def n_digits x
|
96
|
+
sprintf "%010d", x
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get the sequence number from the log file name.
|
100
|
+
#
|
101
|
+
# mylogfile.000034 => 34
|
102
|
+
def log_file_number
|
103
|
+
log_file[/\.(\d+)$/, 1].to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
# Examples of the four identifiers:
|
107
|
+
#
|
108
|
+
# log/type_complete/thelog.000001
|
109
|
+
# log/type_current/log_file_thelog.0000000006/log_position_0000000311/n_parts_0000000001/part_number_0000000000
|
110
|
+
# full/type_mysqldump/log_file_thelog.0000000006/log_position_0000000382/n_parts_0000000001/part_number_0000000000
|
111
|
+
# full/type_binary/log_file_thelog.0000000006/log_position_0000000311/n_parts_0000000001/part_number_0000000000
|
112
|
+
def self.string_to_args s
|
113
|
+
parts = s.split(':')
|
114
|
+
args = {}
|
115
|
+
args[:category] = parts.shift.to_sym
|
116
|
+
args[:type] = (parts.shift)[/^type_(.*)/, 1].to_sym
|
117
|
+
args[:log_file] = (parts.shift)[/log_file_(.*)/, 1]
|
118
|
+
unless parts.empty?
|
119
|
+
args[:log_position] = (parts.shift)[/log_position_(.*)/, 1].to_i
|
120
|
+
end
|
121
|
+
unless parts.empty?
|
122
|
+
args[:n_parts] = (parts.shift)[/n_parts_(.*)/, 1].to_i
|
123
|
+
args[:part_number] = (parts.shift)[/part_number_(.*)/, 1].to_i
|
124
|
+
end
|
125
|
+
args
|
126
|
+
end
|
127
|
+
|
128
|
+
def [] x
|
129
|
+
send x
|
130
|
+
end
|
131
|
+
|
132
|
+
def <=> rhs
|
133
|
+
category <=> rhs.category || self[:type] <=> rhs[:type] || log_file <=> rhs.log_file || log_position <=> rhs.log_position
|
134
|
+
end
|
135
|
+
|
136
|
+
append_factory_method do |args|
|
137
|
+
result = args[:string] && create_object(string_to_args(args[:string]))
|
138
|
+
if result
|
139
|
+
result.timestamp = args[:timestamp]
|
140
|
+
end
|
141
|
+
result
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class MysqlBackup::Entity::Identifier::Full < MysqlBackup::Entity::Identifier
|
146
|
+
end
|
147
|
+
|
148
|
+
class MysqlBackup::Entity::Identifier::Full::Binary < MysqlBackup::Entity::Identifier::Full
|
149
|
+
append_factory_method {|args| args[:category] == :full && args[:type] == :binary && new(args)}
|
150
|
+
end
|
151
|
+
|
152
|
+
class MysqlBackup::Entity::Identifier::Full::Mysqldump < MysqlBackup::Entity::Identifier::Full
|
153
|
+
append_factory_method {|args| args[:category] == :full && args[:type] == :mysqldump && new(args)}
|
154
|
+
end
|
155
|
+
|
156
|
+
class MysqlBackup::Entity::Identifier::Log < MysqlBackup::Entity::Identifier
|
157
|
+
end
|
158
|
+
|
159
|
+
class MysqlBackup::Entity::Identifier::Log::Current < MysqlBackup::Entity::Identifier::Log
|
160
|
+
def to_s
|
161
|
+
pos = n_digits log_position
|
162
|
+
return "#{category}:type_#{@type}:log_file_#{log_file}:log_position_#{pos}"
|
163
|
+
end
|
164
|
+
|
165
|
+
append_factory_method {|args| args[:category] == :log && args[:type] == :current && new(args)}
|
166
|
+
end
|
167
|
+
|
168
|
+
class MysqlBackup::Entity::Identifier::Log::Complete < MysqlBackup::Entity::Identifier::Log
|
169
|
+
def to_s
|
170
|
+
return "#{category}:type_#{@type}:log_file_#{log_file}"
|
171
|
+
end
|
172
|
+
append_factory_method {|args| args[:category] == :log && args[:type] == :complete && new(args)}
|
173
|
+
end
|
174
|
+
|
175
|
+
class Pathname
|
176
|
+
def path
|
177
|
+
to_s
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'named_arguments'
|
3
|
+
require 'mysql_backup/entity/files'
|
4
|
+
|
5
|
+
class MysqlBackup::Entity::Logs < MysqlBackup::Entity::Files
|
6
|
+
include NamedArguments
|
7
|
+
|
8
|
+
attr_accessor :log_bin_dir
|
9
|
+
attr_accessor :completed_logs
|
10
|
+
attr_accessor :log_position
|
11
|
+
attr_accessor :log_file
|
12
|
+
|
13
|
+
# Takes the following arguments:
|
14
|
+
#
|
15
|
+
# :log_bin_dir => The prefix of the log files
|
16
|
+
# :completed_logs => All the logs before the one being written
|
17
|
+
# :log_file => The current log
|
18
|
+
# :log_position => The position in the log file
|
19
|
+
def initialize args = {}
|
20
|
+
super
|
21
|
+
@log_bin_dir = Pathname.new(args[:log_bin_dir])
|
22
|
+
end
|
23
|
+
|
24
|
+
def completed_logs_paths
|
25
|
+
completed_logs.map {|l| Pathname.new(log_bin_dir) + l}
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_file_path
|
29
|
+
Pathname.new(log_bin_dir) + log_file
|
30
|
+
end
|
31
|
+
|
32
|
+
def save args = {}
|
33
|
+
# Do the complete logs
|
34
|
+
completed_logs_paths.each do |p|
|
35
|
+
i = MysqlBackup::Entity::Identifier.create_object :category => :log, :type => :complete, :log_file => p.basename.to_s, :n_parts => 1, :part_number => 0
|
36
|
+
files = self.class.tar_files [p.to_s]
|
37
|
+
yield :identifier => i, :file => files.first
|
38
|
+
end
|
39
|
+
|
40
|
+
# Do the current log
|
41
|
+
i = MysqlBackup::Entity::Identifier.create_object :category => :log, :type => :current, :log_file => log_file, :log_position => log_position, :n_parts => 1, :part_number => 0
|
42
|
+
yield :identifier => i, :file => log_file_path
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'mysql_backup/entity'
|
2
|
+
|
3
|
+
# Mysqldump files are stored gzipped and split
|
4
|
+
class MysqlBackup::Entity::Mysqldump
|
5
|
+
include NamedArguments
|
6
|
+
|
7
|
+
attr_accessor :log
|
8
|
+
attr_accessor :log_file
|
9
|
+
attr_accessor :log_position
|
10
|
+
|
11
|
+
# Create a mysqldump file and yield
|
12
|
+
# a hash with a MysqlBackup::Entity::Identifier.
|
13
|
+
def create
|
14
|
+
Tempfile.open 'mysqldump' do |f|
|
15
|
+
cmd = "mysqldump --opt --all-databases --single-transaction --master-data=2 > #{f.path}"
|
16
|
+
log && log.info("running #{cmd}")
|
17
|
+
system cmd or raise RuntimeError, "failed to run command: #{cmd}"
|
18
|
+
f.flush
|
19
|
+
get_log_position f or raise RuntimeError, "could not get log position"
|
20
|
+
f.seek 0
|
21
|
+
compressed_files = compress_and_split f
|
22
|
+
identifier = MysqlBackup::Entity::Identifier.create_object :category => :full, :type => :mysqldump, :n_parts => compressed_files.length, :log_position => log_position, :log_file => log_file
|
23
|
+
compressed_files.each_with_index do |cf, n|
|
24
|
+
i = identifier.dup
|
25
|
+
i.part_number = n
|
26
|
+
yield :identifier => i, :file => cf
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Search through a mysqldump file looking for a line like
|
32
|
+
# CHANGE MASTER TO MASTER_LOG_FILE='thelog.000005', MASTER_LOG_POS=1163;
|
33
|
+
#
|
34
|
+
# Set log_position and log_file to the corresponding values.
|
35
|
+
def get_log_position file_obj
|
36
|
+
result = {}
|
37
|
+
n = 0
|
38
|
+
file_obj.each_line do |l|
|
39
|
+
if l =~ /CHANGE MASTER TO MASTER_LOG_FILE='(.*)', MASTER_LOG_POS=(\d+)/i
|
40
|
+
@log_file = $1
|
41
|
+
@log_position = $2.to_i
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
n += 1
|
45
|
+
break if n > 100
|
46
|
+
end
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def compress_and_split file_obj
|
51
|
+
destination_name = "#{file_obj.path}xxx"
|
52
|
+
cmd = "cat #{file_obj.path} | gzip | split --suffix-length=4 --bytes=10240000 --numeric-suffixes - #{destination_name}"
|
53
|
+
log && log.info("running " + cmd)
|
54
|
+
system cmd
|
55
|
+
Pathname.glob destination_name + "*"
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pathname'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
require 'mysql_backup/entity/identifier'
|
6
|
+
|
7
|
+
module MysqlBackup; end
|
8
|
+
|
9
|
+
class MysqlBackup::Entity
|
10
|
+
attr_accessor :log
|
11
|
+
|
12
|
+
# Create a set of tarred, gziped, and split files
|
13
|
+
# for all the files given by required_path_strings.
|
14
|
+
#
|
15
|
+
# Return an array of Pathname objects for the
|
16
|
+
# split files.
|
17
|
+
#
|
18
|
+
# The caller is responsible for removing the files returned
|
19
|
+
# from tar_files.
|
20
|
+
def self.tar_files path_strings #:nodoc:
|
21
|
+
result = []
|
22
|
+
# Creating a tempfile to get a guaranteed unique filename.
|
23
|
+
# We don't actually write to this tempfile, we just
|
24
|
+
# use its name as the base name for the split.
|
25
|
+
Tempfile.open 'mysqltarball' do |f|
|
26
|
+
# We should never see existing files, but just in
|
27
|
+
# case we'll need to remove any.
|
28
|
+
Pathname.glob(f.path.to_s + 'xxx*').each {|e| e.unlink}
|
29
|
+
|
30
|
+
paths = path_strings.map {|ps| ps.slice(1..-1)}.join(' ')
|
31
|
+
five_gig = 1024 * 1024 * 1024 * 5
|
32
|
+
tar_cmd = "( cd / ; tar cf - #{paths} | gzip | split --suffix-length=4 --bytes=#{five_gig} --numeric-suffixes - #{f.path}xxx )"
|
33
|
+
do_tar :cmd => tar_cmd
|
34
|
+
result = Pathname.glob(f.path.to_s + 'xxx*')
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'named_arguments'
|
3
|
+
require 'mysql_backup/entity/identifier'
|
4
|
+
|
5
|
+
module MysqlBackup; end
|
6
|
+
class MysqlBackup::Librarian; end
|
7
|
+
|
8
|
+
# A backup is a complete set of identifiers for each run of backup type (full or log).
|
9
|
+
# Each backup has multiple identifiers because a single backup can have multiple files.
|
10
|
+
# The mysql binary logs are tarred up and then split into smaller parts to fit on S3.
|
11
|
+
# The same for mysqldump files; they can be greater than the size you can store in one
|
12
|
+
# S3 bucket.
|
13
|
+
class MysqlBackup::Librarian::Backup
|
14
|
+
include Comparable
|
15
|
+
include FactoryCreateMethod
|
16
|
+
include NamedArguments
|
17
|
+
|
18
|
+
# Returns an array of MysqlBackup::Entity::Identifier objects
|
19
|
+
# that are the members of this group
|
20
|
+
attr_accessor :identifiers
|
21
|
+
attribute_defaults :identifiers => []
|
22
|
+
|
23
|
+
create_class_settings_method :identifier_class
|
24
|
+
|
25
|
+
def initialize args = {}
|
26
|
+
super
|
27
|
+
args[:identifier] or raise RuntimeError, "Must provide :identifier"
|
28
|
+
identifiers << args[:identifier]
|
29
|
+
end
|
30
|
+
|
31
|
+
# True if this backup is the most recent of its type
|
32
|
+
def most_recent?
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Write the files for this backup, untarring and unzipping
|
37
|
+
# as required.
|
38
|
+
def write_files directory
|
39
|
+
end
|
40
|
+
|
41
|
+
# Write the files for this backup without untarring and unzipping.
|
42
|
+
def write_raw_files directory
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if the identifier was added to this group
|
46
|
+
def add_identifier_to_group_if_the_identifier_should_be_in_this_group identifier
|
47
|
+
is_part = is_part_of_this_group? identifier
|
48
|
+
identifiers << identifier if is_part
|
49
|
+
is_part
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_part_of_this_group? identifier
|
53
|
+
return false unless identifier_class?.first === identifier
|
54
|
+
log_file == identifier.log_file && log_position == identifier.log_position
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_file
|
58
|
+
identifiers.first.log_file
|
59
|
+
end
|
60
|
+
|
61
|
+
def log_position
|
62
|
+
identifiers.first.log_position
|
63
|
+
end
|
64
|
+
|
65
|
+
def each_identifier
|
66
|
+
identifiers.each {|i| yield i}
|
67
|
+
end
|
68
|
+
|
69
|
+
def <=> rhs
|
70
|
+
identifiers.first.log_file_number <=> rhs.log_file_number || identifiers.first.log_position <=> rhs.log_position
|
71
|
+
end
|
72
|
+
|
73
|
+
# The id string for a backup doesn't include the number of parts and the
|
74
|
+
# part itself.
|
75
|
+
#
|
76
|
+
# (Split on :, ignore the last two elements)
|
77
|
+
def to_s
|
78
|
+
identifiers.first.to_s.split(':').slice(0..-3).join(':')
|
79
|
+
end
|
80
|
+
|
81
|
+
def name_match rhs
|
82
|
+
to_s == rhs
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.new_if_class_match klass
|
86
|
+
new_if_class klass, :identifier
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module MysqlBackup
|
91
|
+
class Librarian::Backup::Full < Librarian::Backup
|
92
|
+
end
|
93
|
+
|
94
|
+
class Librarian::Backup::Full::Binary < Librarian::Backup::Full
|
95
|
+
new_if_class_match Entity::Identifier::Full::Binary
|
96
|
+
identifier_class Entity::Identifier::Full::Binary
|
97
|
+
end
|
98
|
+
|
99
|
+
class Librarian::Backup::Full::Mysqldump < Librarian::Backup::Full
|
100
|
+
new_if_class_match Entity::Identifier::Full::Mysqldump
|
101
|
+
identifier_class Entity::Identifier::Full::Mysqldump
|
102
|
+
end
|
103
|
+
|
104
|
+
class Librarian::Backup::Log < Librarian::Backup
|
105
|
+
def to_s
|
106
|
+
identifiers.first.to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Librarian::Backup::Log::Current < Librarian::Backup::Log
|
111
|
+
new_if_class_match Entity::Identifier::Log::Current
|
112
|
+
identifier_class Entity::Identifier::Log::Current
|
113
|
+
end
|
114
|
+
|
115
|
+
class Librarian::Backup::Log::Complete < Librarian::Backup::Log
|
116
|
+
new_if_class_match Entity::Identifier::Log::Complete
|
117
|
+
identifier_class Entity::Identifier::Log::Complete
|
118
|
+
|
119
|
+
def is_part_of_this_group? identifier
|
120
|
+
return false unless identifier_class?.first === identifier
|
121
|
+
log_file == identifier.log_file
|
122
|
+
end
|
123
|
+
|
124
|
+
def log_position
|
125
|
+
0
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Symbol
|
131
|
+
def to_proc
|
132
|
+
Proc.new { |obj, *args| obj.send(self, *args) }
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'named_arguments'
|
2
|
+
require 'mysql_backup/librarian/backup'
|
3
|
+
|
4
|
+
# A backup group collection is a list of all the backup groups held by
|
5
|
+
# a MysqlBackup::Storage object.
|
6
|
+
#
|
7
|
+
# Backup group collections:: A collection of backup groups. Usually contains all of the backup groups in an S3 bucket.
|
8
|
+
# Backup group:: A collection of identifiers for S3 buckets. These identifiers point to a a single backup.
|
9
|
+
module MysqlBackup; end
|
10
|
+
class MysqlBackup::Librarian; end
|
11
|
+
|
12
|
+
class MysqlBackup::Librarian::BackupCollection
|
13
|
+
include NamedArguments
|
14
|
+
|
15
|
+
attr_accessor :groups
|
16
|
+
attribute_defaults :groups => []
|
17
|
+
|
18
|
+
def add_backup_group g
|
19
|
+
groups << g
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_identifier identifier
|
23
|
+
result = nil
|
24
|
+
result = groups.find do |g|
|
25
|
+
g.add_identifier_to_group_if_the_identifier_should_be_in_this_group identifier
|
26
|
+
end
|
27
|
+
unless result
|
28
|
+
result = MysqlBackup::Librarian::Backup.create_object :identifier => identifier
|
29
|
+
groups << result
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def groups_matching_type t
|
35
|
+
each_group t
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_log
|
39
|
+
each_group MysqlBackup::Librarian::Backup::Log do |g|
|
40
|
+
yield g
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def each_group matching_type = Object
|
45
|
+
result = []
|
46
|
+
groups.select {|g| matching_type === g}.each do |gg|
|
47
|
+
yield gg if block_given?
|
48
|
+
result << gg
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_group group_name
|
54
|
+
each_group do |g|
|
55
|
+
return g if group_name == g.to_s
|
56
|
+
end
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def types
|
61
|
+
groups.map(&:class).uniq
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'named_arguments'
|
3
|
+
|
4
|
+
require 'mysql_backup/server'
|
5
|
+
require 'mysql_backup/storage/s3'
|
6
|
+
require 'mysql_backup/entity/files'
|
7
|
+
require 'mysql_backup/entity/files/innodb'
|
8
|
+
require 'mysql_backup/entity/files/myisam'
|
9
|
+
require 'mysql_backup/entity/logs'
|
10
|
+
require 'mysql_backup/entity/mysqldump'
|
11
|
+
require 'mysql_backup/librarian/backup_collection'
|
12
|
+
|
13
|
+
# backup_data_files:: Save the binary backups
|
14
|
+
# backup_mysqldump:: Send the mysqldump to s3
|
15
|
+
# backup_binary_logs:: Send the logs to S3
|
16
|
+
class MysqlBackup::Librarian
|
17
|
+
include NamedArguments
|
18
|
+
|
19
|
+
# MySQL connection parameters
|
20
|
+
attr_accessor :host, :user, :password, :db, :port, :sock, :flag
|
21
|
+
|
22
|
+
# The logger. Default is to log via STDERR.
|
23
|
+
attr_accessor :log
|
24
|
+
|
25
|
+
# A MysqlBackup::Storage object.
|
26
|
+
attr_accessor :storage
|
27
|
+
|
28
|
+
# The connection to MySQL
|
29
|
+
attr_accessor :connection
|
30
|
+
|
31
|
+
# The MysqlBackup::Server object used to talk to MySQL.
|
32
|
+
attr_writer :mysql_server
|
33
|
+
|
34
|
+
# S3 parameters
|
35
|
+
attr_accessor :access_key_id, :secret_access_key, :bucket
|
36
|
+
|
37
|
+
# Takes a required argument to specify the location of the log files:
|
38
|
+
#
|
39
|
+
# :log_bin_dir => '/var/lib/mysql'
|
40
|
+
#
|
41
|
+
# Takes required arguments for S3:
|
42
|
+
#
|
43
|
+
# :access_key_id => 'abc',
|
44
|
+
# :secret_access_key => '123'
|
45
|
+
# :bucket => 'name_of_the_backup_bucket'
|
46
|
+
#
|
47
|
+
# Takes these arguments for the connection to MySQL:
|
48
|
+
#
|
49
|
+
# :host
|
50
|
+
# :user
|
51
|
+
# :password
|
52
|
+
# :db
|
53
|
+
# :port
|
54
|
+
# :sock
|
55
|
+
# :flag
|
56
|
+
#
|
57
|
+
# Many installations just need to specify <tt>:host</tt> and <tt>:user</tt>.
|
58
|
+
def initialize args = {}
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
def backup_data_files
|
63
|
+
MysqlBackup::Entity::Files.create_tar_files :log => log, :mysql_server => mysql_server, :mysql_files => [innodb_files, myisam_files] do |args|
|
64
|
+
storage.conditional_save args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def backup_mysqldump
|
69
|
+
m = MysqlBackup::Entity::Mysqldump.new :log => log
|
70
|
+
m.create do |args|
|
71
|
+
storage.conditional_save args
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def backup_binary_logs
|
76
|
+
logs_obj = mysql_server.create_logs_obj
|
77
|
+
logs_obj.save do |args|
|
78
|
+
storage.conditional_save args
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def innodb_files
|
83
|
+
@innodb_files ||= mysql_server.create_innodb_files_obj
|
84
|
+
end
|
85
|
+
|
86
|
+
def myisam_files
|
87
|
+
@myisam_files ||= mysql_server.create_myisam_files_obj
|
88
|
+
end
|
89
|
+
|
90
|
+
def log_files
|
91
|
+
@log_files ||= mysql_server.create_logs_obj
|
92
|
+
end
|
93
|
+
|
94
|
+
def mysql_server
|
95
|
+
@mysql_server ||= MysqlBackup::Server.new :connection => create_connection, :log => log
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_backup_group_collection_from_storage
|
99
|
+
unless @backups
|
100
|
+
c = MysqlBackup::Librarian::BackupCollection.new
|
101
|
+
storage.yield_identifiers do |i|
|
102
|
+
c.add_identifier i
|
103
|
+
end
|
104
|
+
@backups = c
|
105
|
+
end
|
106
|
+
@backups
|
107
|
+
end
|
108
|
+
|
109
|
+
def ls klass = Object
|
110
|
+
result = []
|
111
|
+
create_backup_group_collection_from_storage.each_group(klass) do |g|
|
112
|
+
result << g.to_s
|
113
|
+
end
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
def rm backup_name
|
118
|
+
g = find_group backup_name
|
119
|
+
g.each_identifier do |i|
|
120
|
+
storage.rm i
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def get f, directory
|
125
|
+
case f
|
126
|
+
when /^full:type_binary/
|
127
|
+
get_full_binary f, directory
|
128
|
+
when /^full:type_mysqldump/
|
129
|
+
get_mysqldump f, directory
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_full_binary f, destination_dir = '/tmp'
|
134
|
+
get_backup f do |tempfile|
|
135
|
+
run_cmd "( cd #{destination_dir}; zcat #{tempfile.path} | tar xf - )"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def get_mysqldump f, destination_dir = '/tmp'
|
140
|
+
get_backup f do |tempfile|
|
141
|
+
run_cmd "( cd #{destination_dir}; zcat #{tempfile.path} > #{f})"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def get_logs destination_dir = '/tmp'
|
146
|
+
create_backup_group_collection_from_storage.each_log do |g|
|
147
|
+
log && log.info("Looking at backup #{g.to_s}")
|
148
|
+
storage.retrieve_backup_and_then_yield_file g do |tempfile|
|
149
|
+
run_cmd "( cd #{destination_dir}; zcat #{tempfile.path} | tar xf - )"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def run_cmd cmd
|
155
|
+
log && log.debug("Run command: #{cmd}")
|
156
|
+
system cmd
|
157
|
+
end
|
158
|
+
|
159
|
+
# Always call this with a block that will do something
|
160
|
+
# with the raw data retrieved from S3.
|
161
|
+
def get_backup f, &block
|
162
|
+
b = find_group f
|
163
|
+
raise RuntimeError, "Failed to find backup for #{f}" unless b
|
164
|
+
if b
|
165
|
+
storage.retrieve_backup_and_then_yield_file(b, &block)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def find_group f
|
170
|
+
create_backup_group_collection_from_storage.find_group f
|
171
|
+
end
|
172
|
+
|
173
|
+
def create_connection
|
174
|
+
@connection ||= Mysql.connect(@host, @user, @pass, @db, @port, @sock, @flag)
|
175
|
+
end
|
176
|
+
end
|