ey_cloud_server 1.3.1 → 1.4.5.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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,253 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Filename: binary_log_purge.rb
|
4
|
+
# Author: Tyler Poland
|
5
|
+
# Version: 0.2
|
6
|
+
# Purpose: Script to check current state of all replica databases and
|
7
|
+
# purge binary logs from the master based on the position of any
|
8
|
+
# and all replica databases.
|
9
|
+
|
10
|
+
# Changelog 0.1 -> 0.2
|
11
|
+
# - modified binlog check routine to lookup binary log storage in configuration file instead of relying on
|
12
|
+
# binary logs being stored in the data directory
|
13
|
+
|
14
|
+
# Changelog 0.2 -> 0.3
|
15
|
+
# - Added ability to purge binary logs for standalone master databases
|
16
|
+
|
17
|
+
# Changelog 0.3 -> 0.4
|
18
|
+
# - Added support for remote tunneled slave with reverse connection on port 13306
|
19
|
+
|
20
|
+
# Changelog 0.4 -> 1.0
|
21
|
+
# - Add automatic create user for master to connect to replica
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
|
25
|
+
require 'net/smtp'
|
26
|
+
require 'mysql'
|
27
|
+
require 'yaml'
|
28
|
+
require 'open3'
|
29
|
+
require 'getoptlong'
|
30
|
+
|
31
|
+
# Set up logging functions based on desired verbosity level
|
32
|
+
def log_info(message) # may get redefined below
|
33
|
+
puts message
|
34
|
+
end
|
35
|
+
|
36
|
+
def log_error(message)
|
37
|
+
STDERR.write(message + "\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
opts = GetoptLong.new(['--quiet', '-q', GetoptLong::NO_ARGUMENT])
|
41
|
+
opts.each do |opt, arg|
|
42
|
+
if opt == '--quiet'
|
43
|
+
def log_info(_) end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
log_info Time.now
|
49
|
+
# Conditional require for JSON if dna file exists
|
50
|
+
chef_file = '/etc/chef/dna.json'
|
51
|
+
if File.exists?(chef_file)
|
52
|
+
require 'rubygems'
|
53
|
+
require 'json'
|
54
|
+
end
|
55
|
+
|
56
|
+
# Modify default purge count
|
57
|
+
keep_logs = 5
|
58
|
+
binpurge_config = '/etc/engineyard/binlogpurge.yml'
|
59
|
+
if File.exists?(binpurge_config)
|
60
|
+
options = YAML::load(File.read(binpurge_config))
|
61
|
+
if options['keep'] > 0
|
62
|
+
keep_logs = options['keep']
|
63
|
+
log_info "Overriding keep logs from configuration file"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# function to send error emails
|
68
|
+
def failure_message(message)
|
69
|
+
sender = "Database Team <db@engineyard.com"
|
70
|
+
recipients = "db@engineyard.com"
|
71
|
+
hostname = `hostname`.chomp
|
72
|
+
subject = "An error has occurred while purging binary logs on #{hostname}"
|
73
|
+
mailtext = <<EOF
|
74
|
+
From: #{sender}
|
75
|
+
To: #{recipients}
|
76
|
+
Subject: #{subject}
|
77
|
+
#{message}
|
78
|
+
EOF
|
79
|
+
|
80
|
+
begin Net::SMTP.start('mail') do |smtp|
|
81
|
+
smtp.sendmail(mailtext, 'root@' + hostname, recipients)
|
82
|
+
end
|
83
|
+
rescue Exception => e
|
84
|
+
log_error "Exception occurred: " + e
|
85
|
+
end
|
86
|
+
|
87
|
+
exit(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
# function to retrieve password from .mytop file
|
91
|
+
def get_password
|
92
|
+
dbpass = %x{cat /root/.mytop |grep pass |awk -F= '{print $2}'}.chomp
|
93
|
+
failure_message() if dbpass.length < 1
|
94
|
+
dbpass
|
95
|
+
end
|
96
|
+
|
97
|
+
# function to run query against database
|
98
|
+
def run_query(host, user, password, query)
|
99
|
+
options = ''
|
100
|
+
if host == '127.0.0.1'
|
101
|
+
options = options + ' -P13306'
|
102
|
+
end
|
103
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} #{options} -h#{host} -N -e\"#{query}\"")
|
104
|
+
query_error = stderr.read
|
105
|
+
if query_error.length > 0
|
106
|
+
log_error "Error caught: #{query_error}"
|
107
|
+
test_add_privilege(user, password, query_error)
|
108
|
+
exit 0
|
109
|
+
end
|
110
|
+
rs = stdout.read
|
111
|
+
end
|
112
|
+
|
113
|
+
# function to test for user privilege
|
114
|
+
def test_add_privilege(user, password, error)
|
115
|
+
full_hostname = %x{hostname --long}.chomp
|
116
|
+
dns_name = %x{hostname -d}.chomp
|
117
|
+
# verify that this is the user privilege error with the root user not having access to the replica
|
118
|
+
if error.match(/ERROR 1045.* Access denied for user 'root'@'.*#{dns_name}' \(using password: YES\)/)
|
119
|
+
# check the master to see if grant based on hostname or IP exists
|
120
|
+
master_ip = %x{hostname -i}.chomp.gsub(/\s+/,'')
|
121
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} -e\"show grants for 'root'@'#{master_ip}'\"")
|
122
|
+
master_ip_error = stderr.read
|
123
|
+
|
124
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} -e\"show grants for 'root'@'#{full_hostname}'\"")
|
125
|
+
full_hostname_error = stderr.read
|
126
|
+
|
127
|
+
regex = 'ERROR 1141.*There is no such grant defined'
|
128
|
+
if master_ip_error.match(/#{regex}/) || full_hostname_error.match(/#{regex}/)
|
129
|
+
# neither grant is defined on the master so go ahead and add it
|
130
|
+
log_info "The user privilege does not exist on the master, the script will now create it."
|
131
|
+
log_info "This privilege must propagate to the replica via replication, the user may not be available for immediate use."
|
132
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} -e\"grant all privileges on *.* to 'root'@'#{master_ip}' identified by '#{password}'\"")
|
133
|
+
create_user_error = stderr.read
|
134
|
+
if create_user_error.length > 0
|
135
|
+
log_error "Unable to create user: #{create_user_error}"
|
136
|
+
exit 1
|
137
|
+
end
|
138
|
+
else
|
139
|
+
log_error "The required privilege appears to exist on the master, you may need to wait for replication to process the grant on the Replica"
|
140
|
+
exit 0
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# function to convert input into yaml
|
146
|
+
def yaml_result(file)
|
147
|
+
parse = file.gsub(/^\*.*$/,'').gsub(/^/,' ').gsub(/^\s+/, ' ')
|
148
|
+
yml = YAML.load(parse)
|
149
|
+
end
|
150
|
+
|
151
|
+
# parse the hostname out of the processlist
|
152
|
+
def extract_host(line)
|
153
|
+
line =~ /.+\s+(.+):.+/
|
154
|
+
$1
|
155
|
+
end
|
156
|
+
|
157
|
+
# function to get replica position from replica host
|
158
|
+
def slave_log_file(hostname, user, pass)
|
159
|
+
if hostname == 'localhost'
|
160
|
+
hostname = '127.0.0.1'
|
161
|
+
end
|
162
|
+
|
163
|
+
q_result = run_query(hostname, user, pass, "show slave status\\G")
|
164
|
+
if q_result.match(/.*Slave_SQL_Running: No.*/)
|
165
|
+
log_error "Slave SQL thread is not running."
|
166
|
+
log_info "The error is: \n#{q_result}"
|
167
|
+
log_error "Unable to continue; exiting!"
|
168
|
+
exit 1
|
169
|
+
end
|
170
|
+
|
171
|
+
yaml = yaml_result(q_result)
|
172
|
+
yaml["Relay_Master_Log_File"]
|
173
|
+
end
|
174
|
+
|
175
|
+
dbuser = 'root'
|
176
|
+
|
177
|
+
if not dbpassword = get_password
|
178
|
+
failure_message("Password not found for slice, check for /root/.mytop")
|
179
|
+
end
|
180
|
+
|
181
|
+
mysql_process = %x{ps -ef|grep '[m]ysqld'}.split(/\s+/).select{|item| item.match(/^--/)}
|
182
|
+
mysql_params = Hash.new
|
183
|
+
mysql_process.each {|i| k, v = i.split('='); mysql_params[k]=v}
|
184
|
+
datadir = mysql_params['--datadir']
|
185
|
+
config_file = mysql_params['--defaults-file']
|
186
|
+
binlog_dir = File.dirname(%x{cat #{config_file} |egrep '^log-bin'|grep -v '.index'|awk -F= '{print $2}'}.gsub(/\s+/,'').chomp)
|
187
|
+
|
188
|
+
|
189
|
+
# If master-bin.000001 exists then only purge logs if disk space is constrained
|
190
|
+
binary_log_name = %x{cat #{config_file}|grep '^log-bin'|grep -v 'index' |awk -F= '{print $2}'}.gsub(/\s/,'')
|
191
|
+
if binary_log_name == ''
|
192
|
+
log_info "log-bin not set in config file, host does not have master role, unable to proceed"
|
193
|
+
exit(0)
|
194
|
+
end
|
195
|
+
if File.exists?(binary_log_name + '.000001')
|
196
|
+
purge_threshold = 90
|
197
|
+
if %x{df -h |egrep "/db" |awk '{print $5}'}.to_i < purge_threshold
|
198
|
+
log_info "The first binary log exists and the purge threshold has not been reached; skipping purge action"
|
199
|
+
exit(0)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check master for all connected replication slaves
|
204
|
+
result = run_query('localhost', dbuser, dbpassword, 'show processlist')
|
205
|
+
slave_hosts = []
|
206
|
+
min_log = 0
|
207
|
+
result.each do |line|
|
208
|
+
if line.include? 'Binlog Dump'
|
209
|
+
slave = Hash.new
|
210
|
+
slave["hostname"] = extract_host(line)
|
211
|
+
slave["Relay_Master_Log_File"] = slave_log_file(slave["hostname"], dbuser, dbpassword)
|
212
|
+
slave["Relay_Master_Log_File"] =~ /\w+.(\d{6})/ and min_log = $1.to_i if $1.to_i < min_log || min_log == 0
|
213
|
+
log_info "Slave Hostname: #{slave["hostname"]}, Relay_Master_log: #{slave["Relay_Master_Log_File"]}"
|
214
|
+
slave_hosts << slave
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# stop log purge #{keep_logs} logs before the current read position
|
219
|
+
stop_log = min_log - keep_logs
|
220
|
+
|
221
|
+
# if standalone master and no replicas are found we stop purge #{keep_logs} logs before master's current position
|
222
|
+
if min_log == 0 and File.exists?(chef_file)
|
223
|
+
chef_config = JSON.parse(File.read(chef_file))
|
224
|
+
if chef_config['db_slaves'].empty? or chef_config['db_slaves'].nil?
|
225
|
+
current_master = %x{cd #{binlog_dir} && ls -tr master-bin.[0-9]* | tail -n 1}
|
226
|
+
current_master =~ /\w+.(\d{6})/ and stop_log = $1.to_i + 1 - keep_logs
|
227
|
+
elsif min_log == 0
|
228
|
+
log_error "Slave is on record as '#{chef_config['db_slaves']}' but replication is not running."
|
229
|
+
exit 1
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
# Purge logs based on minimum position of all servers
|
235
|
+
min_master_log = %x{cd #{binlog_dir} && ls -tr master-bin.[0-9]* | head -n 1}
|
236
|
+
min_master_log =~ /\w+.(\d{6})/ and min_master_num = $1.to_i + 1
|
237
|
+
|
238
|
+
min_master_num.upto(min_master_num + 10) do |i|
|
239
|
+
# purge up to 10 files as long as the top file is less than the minimum replica log
|
240
|
+
if stop_log < 0
|
241
|
+
log_error "Could not verify replication status, confirm that replication is running. Exiting!"
|
242
|
+
break
|
243
|
+
elsif i >= stop_log + 1
|
244
|
+
log_info "File number of #{i} exceeds minimum purge file of #{stop_log + 1} based on keeping #{keep_logs} files. Exiting!"
|
245
|
+
break
|
246
|
+
end
|
247
|
+
file = "master-bin.%06d" % i
|
248
|
+
log_info "Purging binary logs to #{file}"
|
249
|
+
run_query('localhost', dbuser, dbpassword, "purge master logs to '#{file}'")
|
250
|
+
sleep 120
|
251
|
+
end
|
252
|
+
|
253
|
+
log_info Time.now
|
data/bin/ey-snapshots
CHANGED
data/bin/eybackup
CHANGED
data/lib/ey-flex.rb
CHANGED
@@ -18,6 +18,17 @@ require 'open4'
|
|
18
18
|
lib_dir = File.expand_path(__FILE__ + '/../ey-flex')
|
19
19
|
|
20
20
|
module EY
|
21
|
+
def self.notify_error(error)
|
22
|
+
enzyme_api.notify_error("user", error)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.enzyme_api
|
26
|
+
return @enzyme_api if @enzyme_api
|
27
|
+
require 'ey_enzyme'
|
28
|
+
config = YAML.load_file("/etc/engineyard/dracul.yml")
|
29
|
+
@enzyme_api = EY::Enzyme::API.new(config[:api], config[:instance_id], config[:token])
|
30
|
+
end
|
31
|
+
|
21
32
|
module Flex
|
22
33
|
class Error < StandardError; end
|
23
34
|
end
|
@@ -25,10 +36,7 @@ module EY
|
|
25
36
|
end
|
26
37
|
|
27
38
|
require lib_dir + '/big-brother'
|
28
|
-
require lib_dir + '/backups'
|
29
39
|
require lib_dir + '/bucket_minder'
|
30
40
|
require lib_dir + '/ey-api'
|
31
|
-
require lib_dir + '/mysql_database'
|
32
|
-
require lib_dir + '/postgresql_database'
|
33
41
|
require lib_dir + '/snapshot_minder'
|
34
42
|
require lib_dir + '/version'
|
@@ -11,6 +11,7 @@ require 'open-uri'
|
|
11
11
|
module EY
|
12
12
|
|
13
13
|
class BucketMinder
|
14
|
+
attr_accessor :bucket, :name
|
14
15
|
|
15
16
|
def initialize(opts={})
|
16
17
|
AWS::S3::Base.establish_connection!(
|
@@ -21,18 +22,23 @@ module EY
|
|
21
22
|
@type = opts[:type]
|
22
23
|
@env = opts[:env]
|
23
24
|
@opts = opts
|
25
|
+
@bucket = find_bucket(opts[:bucket])
|
26
|
+
|
24
27
|
opts[:extension] ||= "tgz"
|
25
28
|
@keep = opts[:keep]
|
26
29
|
@name = "#{Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')}.#{@type}.#{opts[:extension]}"
|
27
30
|
end
|
28
31
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def find_bucket(buck="#{@env}-#{@type}-#{@instance_id}-#{Digest::SHA1.hexdigest(@opts[:aws_secret_id])[0..6]}")
|
33
|
+
begin
|
34
|
+
begin
|
35
|
+
AWS::S3::Bucket.find(buck)
|
36
|
+
rescue AWS::S3::NoSuchBucket
|
37
|
+
begin
|
38
|
+
AWS::S3::Bucket.create(buck)
|
39
|
+
rescue AWS::S3::ResponseError
|
40
|
+
end
|
41
|
+
end
|
36
42
|
buck
|
37
43
|
end
|
38
44
|
end
|
@@ -41,18 +47,37 @@ module EY
|
|
41
47
|
@instance_id ||= open("http://169.254.169.254/latest/meta-data/instance-id").read
|
42
48
|
end
|
43
49
|
|
44
|
-
def upload_object(file)
|
50
|
+
def upload_object(file, printer = true)
|
45
51
|
AWS::S3::S3Object.store(
|
46
52
|
@name,
|
47
|
-
open(file),
|
53
|
+
File.open(file),
|
48
54
|
bucket,
|
49
55
|
:access => :private
|
50
56
|
)
|
51
57
|
FileUtils.rm file
|
52
|
-
puts "successful upload: #{@name}"
|
58
|
+
puts "successful upload: #{@name}" if printer
|
53
59
|
true
|
54
60
|
end
|
55
61
|
|
62
|
+
def remove_object(key, printer = false)
|
63
|
+
begin
|
64
|
+
AWS::S3::S3Object.delete(key, @bucket)
|
65
|
+
puts "Deleting #{key}" if printer
|
66
|
+
# S3's eventual consistency sometimes causes really weird
|
67
|
+
# failures.
|
68
|
+
# Since cleanup happens every time and will clean up all stale
|
69
|
+
# objects, we can just ignore S3-interaction failures. It'll
|
70
|
+
# work next time.
|
71
|
+
rescue AWS::S3::S3Exception, AWS::S3::Error
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def retrieve_object(key, printer = false)
|
77
|
+
puts "Retrieving #{key}" if printer
|
78
|
+
obj = AWS::S3::S3Object.find(key, @bucket)
|
79
|
+
end
|
80
|
+
|
56
81
|
def download(index, printer = false)
|
57
82
|
obj = list[index.to_i]
|
58
83
|
puts "downloading: #{obj}" if printer
|
@@ -64,6 +89,12 @@ module EY
|
|
64
89
|
puts "finished" if printer
|
65
90
|
obj.key
|
66
91
|
end
|
92
|
+
|
93
|
+
def stream(key)
|
94
|
+
AWS::S3::S3Object.stream(key, @bucket) do |chunk|
|
95
|
+
yield chunk
|
96
|
+
end
|
97
|
+
end
|
67
98
|
|
68
99
|
def cleanup
|
69
100
|
begin
|
@@ -86,13 +117,15 @@ module EY
|
|
86
117
|
File.expand_path(name)
|
87
118
|
end
|
88
119
|
|
120
|
+
# Removes all of the items in the current bucket
|
89
121
|
def clear_bucket
|
90
122
|
list.each do |o|
|
91
|
-
puts "deleting: #{o.key}"
|
123
|
+
puts "deleting: #{o.key}"
|
92
124
|
o.delete
|
93
125
|
end
|
94
126
|
end
|
95
127
|
|
128
|
+
# Deletes the most recent file from the bucket
|
96
129
|
def rollback
|
97
130
|
o = list.last
|
98
131
|
puts "rolling back: #{o.key}"
|
@@ -103,17 +136,59 @@ module EY
|
|
103
136
|
list.empty?
|
104
137
|
end
|
105
138
|
|
106
|
-
def list(
|
107
|
-
|
139
|
+
def list(directive = false)
|
140
|
+
if directive.is_a? Hash
|
141
|
+
prefix = directive[:prefix]
|
142
|
+
printer = directive[:print] ||= false
|
143
|
+
listing = AWS::S3::Bucket.objects(bucket, :prefix => prefix).flatten.sort
|
144
|
+
objects = s3merge(listing)
|
145
|
+
elsif directive.is_a? String
|
146
|
+
printer = directive
|
147
|
+
objects = AWS::S3::Bucket.objects(bucket).sort
|
148
|
+
else
|
149
|
+
puts "Input to list method must be Hash or String not: #{printer.class}"
|
150
|
+
exit
|
151
|
+
end
|
152
|
+
|
153
|
+
return objects if objects.empty?
|
154
|
+
|
108
155
|
puts "listing bucket #{bucket}" if printer && !objects.empty?
|
156
|
+
|
109
157
|
if printer
|
110
|
-
objects.each_with_index do |b,i|
|
111
|
-
|
158
|
+
objects.each_with_index do |b, i|
|
159
|
+
if b.is_a? Hash
|
160
|
+
puts "#{i}:#{@env} #{b[:name]}"
|
161
|
+
else
|
162
|
+
puts "#{i}:#{@env} #{b.key}"
|
163
|
+
end
|
112
164
|
end
|
113
165
|
end
|
114
166
|
objects
|
115
167
|
end
|
116
168
|
|
169
|
+
# Merge s3 file listing to work with split files with naming of *.part\d\d
|
170
|
+
def s3merge(list)
|
171
|
+
return list if list.empty?
|
172
|
+
bucketfiles=Array.new()
|
173
|
+
list.each do |item|
|
174
|
+
fname = item.key.gsub(/.part\d+$/,'')
|
175
|
+
match = false
|
176
|
+
bucketfiles.each_with_index do |b, i|
|
177
|
+
if b[:name] == fname
|
178
|
+
bucketfiles[i][:keys] << item.key
|
179
|
+
match = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
if not match
|
184
|
+
path = Array.new()
|
185
|
+
path << item.key
|
186
|
+
file = {:name => fname, :keys => path}
|
187
|
+
bucketfiles << file
|
188
|
+
end
|
189
|
+
end
|
190
|
+
bucketfiles
|
191
|
+
end
|
192
|
+
|
117
193
|
end
|
118
|
-
|
119
194
|
end
|