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