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.
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ for f in /db/mysql/master-bin*;
4
+ do
5
+ ionice -c3 rm -f $f;
6
+ sleep 30;
7
+ done
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  require File.dirname(__FILE__) + '/../lib/ey-flex'
3
3
 
4
- EY::SnapshotMinder.run(ARGV)
4
+ begin
5
+ EY::SnapshotMinder.run(ARGV)
6
+ rescue => e
7
+ EY.notify_error(e)
8
+ raise
9
+ end
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require File.dirname(__FILE__) + '/../lib/ey-flex'
3
+ require 'rubygems'
4
+ require File.dirname(__FILE__) + '/../lib/ey_backup'
4
5
 
5
- EY::Flex::Backups.run(ARGV)
6
+ EY::Backup.run(ARGV)
@@ -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 bucket
30
- @bucket ||= begin
31
- buck = "#{@env}-#{@type}-#{instance_id}-#{Digest::SHA1.hexdigest(@opts[:aws_secret_id])[0..6]}"
32
- begin
33
- AWS::S3::Bucket.create buck
34
- rescue AWS::S3::ResponseError
35
- end
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(printer = false)
107
- objects = AWS::S3::Bucket.objects(bucket).sort
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
- puts "#{i}:#{@env} #{b.key}"
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