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.
@@ -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