ey-flex-test 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +48 -0
- data/TODO +4 -0
- data/bin/ey-agent +17 -0
- data/bin/ey-monitor +10 -0
- data/bin/ey-recipes +133 -0
- data/bin/ey-slave +7 -0
- data/bin/ey-snapshots +52 -0
- data/bin/eybackup +70 -0
- data/lib/big-brother.rb +66 -0
- data/lib/bucket_minder.rb +110 -0
- data/lib/ey-api.rb +22 -0
- data/lib/ey.rb +305 -0
- data/lib/mysql_backup.rb +127 -0
- data/lib/mysql_slave.rb +22 -0
- data/lib/postgresql_backup.rb +31 -0
- data/lib/snapshot_minder.rb +157 -0
- data/lib/stonith.rb +194 -0
- data/spec/ey_api_spec.rb +74 -0
- metadata +75 -0
data/lib/mysql_slave.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), 'snapshot_minder')
|
3
|
+
require 'json'
|
4
|
+
module EY
|
5
|
+
class MysqlSlave
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@dna = ::JSON.parse(IO.read('/etc/chef/dna.json'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def bootstrap
|
12
|
+
puts("reading in /db/mysql/master_status.json to get position in the binlog")
|
13
|
+
master_status = ::JSON.parse(IO.read('/db/mysql/master_status.json'))
|
14
|
+
master_status["master_host"] = @dna['db_host']
|
15
|
+
master_status["master_user"] = 'replication'
|
16
|
+
master_status["master_password"] = @dna['users'].first['password']
|
17
|
+
@db = ::EY::Mysql.new('root', @dna['users'].first['password'])
|
18
|
+
@db.change_master(master_status)
|
19
|
+
@db.slave_start
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EyBackup
|
2
|
+
class PostgresqlBackup < MysqlBackup
|
3
|
+
def backup_database(database)
|
4
|
+
full_path_to_backup = "#{self.backup_dir}/#{database}.#{@tmpname}"
|
5
|
+
posgrecmd = "PGPASSWORD='#{@dbpass}' pg_dump --clean --no-owner --no-privileges -U#{@dbuser} #{database} | gzip - > #{full_path_to_backup}"
|
6
|
+
if system(posgrecmd)
|
7
|
+
AWS::S3::S3Object.store(
|
8
|
+
"/#{@id}.#{database}/#{database}.#{@tmpname}",
|
9
|
+
open(full_path_to_backup),
|
10
|
+
@bucket,
|
11
|
+
:access => :private
|
12
|
+
)
|
13
|
+
FileUtils.rm full_path_to_backup
|
14
|
+
puts "successful backup: #{database}.#{@tmpname}"
|
15
|
+
else
|
16
|
+
raise "Unable to dump database#{database} wtf?"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def restore(index)
|
21
|
+
name = download(index)
|
22
|
+
db = name.split('.').first
|
23
|
+
cmd = "gunzip -c #{name} | PGPASSWORD='#{@dbpass}' psql -U#{@dbuser} #{db}"
|
24
|
+
if system(cmd)
|
25
|
+
puts "successfully restored backup: #{name}"
|
26
|
+
else
|
27
|
+
puts "FAIL"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'dbi'
|
4
|
+
require 'json'
|
5
|
+
module EY
|
6
|
+
class SnapshotMinder
|
7
|
+
def initialize(opts={})
|
8
|
+
@opts = opts
|
9
|
+
@instance_id = opts[:instance_id]
|
10
|
+
@db = Mysql.new('root', opts[:dbpass]) rescue nil
|
11
|
+
@ec2 = RightAws::Ec2.new(opts[:aws_secret_id], opts[:aws_secret_key])
|
12
|
+
get_instance_id
|
13
|
+
find_volume_ids
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_volume_ids
|
17
|
+
@volume_ids = {}
|
18
|
+
@ec2.describe_volumes.each do |volume|
|
19
|
+
if volume[:aws_instance_id] == @instance_id
|
20
|
+
if volume[:aws_device] == "/dev/sdz1"
|
21
|
+
@volume_ids[:data] = volume[:aws_id]
|
22
|
+
elsif volume[:aws_device] == "/dev/sdz2"
|
23
|
+
@volume_ids[:db] = volume[:aws_id]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
puts("Volume IDs are #{@volume_ids.inspect}")
|
28
|
+
@volume_ids
|
29
|
+
end
|
30
|
+
|
31
|
+
def list_snapshots
|
32
|
+
@snapshot_ids = {}
|
33
|
+
@ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
34
|
+
@volume_ids.each do |mnt, vol|
|
35
|
+
if snapshot[:aws_volume_id] == vol
|
36
|
+
(@snapshot_ids[mnt] ||= []) << snapshot[:aws_id]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
puts("Snapshots #{@snapshot_ids.inspect}")
|
41
|
+
@snapshot_ids
|
42
|
+
end
|
43
|
+
|
44
|
+
def clean_snapshots(keep=5)
|
45
|
+
list_snapshots
|
46
|
+
@snapshot_ids.each do |mnt, ids|
|
47
|
+
snaps = []
|
48
|
+
@ec2.describe_snapshots(ids).sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
49
|
+
snaps << snapshot
|
50
|
+
end
|
51
|
+
(snaps[keep..-1]||[]).each do |snapshot|
|
52
|
+
puts "deleting snapshot of /#{mnt}: #{snapshot[:aws_id]}"
|
53
|
+
@ec2.delete_snapshot(snapshot[:aws_id])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
list_snapshots
|
57
|
+
end
|
58
|
+
|
59
|
+
def snapshot_volumes
|
60
|
+
snaps = []
|
61
|
+
@volume_ids.each do |vol, vid|
|
62
|
+
case vol
|
63
|
+
when :data
|
64
|
+
snaps << create_snapshot(vid)
|
65
|
+
when :db
|
66
|
+
@db.flush_tables_with_read_lock
|
67
|
+
master_status = @db.show_master_status
|
68
|
+
ms_json = File.open('/db/mysql/master_status.json', "w")
|
69
|
+
JSON.dump(master_status, ms_json)
|
70
|
+
ms_json.fsync
|
71
|
+
ms_json.close
|
72
|
+
snaps << create_snapshot(vid)
|
73
|
+
@db.unlock_tables
|
74
|
+
end
|
75
|
+
end
|
76
|
+
snaps
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_instance_id
|
80
|
+
return @instance_id if @instance_id
|
81
|
+
|
82
|
+
open('http://169.254.169.254/latest/meta-data/instance-id') do |f|
|
83
|
+
@instance_id = f.gets
|
84
|
+
end
|
85
|
+
raise "Cannot find instance id!" unless @instance_id
|
86
|
+
puts("Instance ID is #{@instance_id}")
|
87
|
+
@instance_id
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def create_snapshot(volume_id)
|
92
|
+
snap = @ec2.create_snapshot(volume_id)
|
93
|
+
puts("Created snapshot of #{volume_id} as #{snap[:aws_id]}")
|
94
|
+
snap
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class Mysql
|
100
|
+
|
101
|
+
attr_accessor :dbh
|
102
|
+
|
103
|
+
def initialize(username, password)
|
104
|
+
@username = username
|
105
|
+
@password = password
|
106
|
+
puts("Connecting to MySQL")
|
107
|
+
@dbh = DBI.connect("DBI:Mysql:mysql", username, password)
|
108
|
+
end
|
109
|
+
|
110
|
+
def show_master_status
|
111
|
+
status = Hash.new
|
112
|
+
@dbh.select_all("show master status") do |row|
|
113
|
+
row.each_with_name do |v, column|
|
114
|
+
status[column] = v
|
115
|
+
end
|
116
|
+
end
|
117
|
+
puts("Master status: #{status.inspect}")
|
118
|
+
status
|
119
|
+
end
|
120
|
+
|
121
|
+
def change_master(master_status)
|
122
|
+
command = "CHANGE MASTER TO"
|
123
|
+
command << " MASTER_HOST='#{master_status['master_host']}',"
|
124
|
+
command << " MASTER_USER='#{master_status['master_user']}',"
|
125
|
+
command << " MASTER_PASSWORD='#{master_status['master_password']}',"
|
126
|
+
command << " MASTER_LOG_FILE='#{master_status['File']}',"
|
127
|
+
command << " MASTER_LOG_POS=#{master_status['Position']}"
|
128
|
+
puts(command)
|
129
|
+
@dbh.do(command)
|
130
|
+
puts("Master is now #{master_status['master_host']} at #{master_status['File']} pos #{master_status['Position']}")
|
131
|
+
end
|
132
|
+
|
133
|
+
def slave_start
|
134
|
+
@dbh.do("slave start")
|
135
|
+
puts("Slave started")
|
136
|
+
end
|
137
|
+
|
138
|
+
def flush_tables_with_read_lock
|
139
|
+
puts("Flushing tables with read lock")
|
140
|
+
@dbh.do("flush tables with read lock")
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def unlock_tables
|
145
|
+
puts("Unlocking tables")
|
146
|
+
@dbh.do("unlock tables")
|
147
|
+
true
|
148
|
+
end
|
149
|
+
|
150
|
+
def disconnect
|
151
|
+
puts("Disconnecting from MySQL")
|
152
|
+
@dbh.disconnect
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
data/lib/stonith.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
# gem install igrigorik-em-http-request --source http://gems.github.com
|
4
|
+
gem 'igrigorik-em-http-request'
|
5
|
+
require 'em-http'
|
6
|
+
require 'json/ext'
|
7
|
+
#require 'ey-api'
|
8
|
+
require 'right_aws'
|
9
|
+
require 'open-uri'
|
10
|
+
require 'rest_client'
|
11
|
+
require 'dbi'
|
12
|
+
require 'ey-api'
|
13
|
+
|
14
|
+
module EY
|
15
|
+
class Log
|
16
|
+
def self.write(str)
|
17
|
+
puts str
|
18
|
+
File.open("/root/ey-monitor2.log", "a") do |f|
|
19
|
+
f.write("#{str}\n")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Stonith
|
25
|
+
include EyApi
|
26
|
+
def initialize(opts={})
|
27
|
+
Log.write "Starting up"
|
28
|
+
@opts = opts
|
29
|
+
@rest = RestClient::Resource.new(opts[:api])
|
30
|
+
@keys = {:aws_secret_id => @opts[:aws_secret_id], :aws_secret_key => @opts[:aws_secret_key]}
|
31
|
+
@bad_checks = 0
|
32
|
+
@seen_good_check = false
|
33
|
+
@ec2 = RightAws::Ec2.new(@opts[:aws_secret_id], @opts[:aws_secret_key])
|
34
|
+
@taking_over = false
|
35
|
+
get_local_json
|
36
|
+
get_master_from_json
|
37
|
+
setup_traps
|
38
|
+
start
|
39
|
+
am_i_master?
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup_traps
|
43
|
+
trap("HUP") { cancel_master_check_timer; Log.write "timer canceled, not monitoring until you wake me up again"}
|
44
|
+
trap("USR1") { EM.add_timer(600) { setup_master_check_timer unless am_i_master? }; Log.write "woke up, starting monitoring again in 10 minutes"}
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_mysql_handle
|
48
|
+
DBI.connect("DBI:Mysql:engineyard:#{@json['db_host']}", 'root', @opts[:dbpass])
|
49
|
+
end
|
50
|
+
|
51
|
+
def try_lock(nodename)
|
52
|
+
Log.write("Trying to grab the lock for: #{nodename}")
|
53
|
+
db = get_mysql_handle
|
54
|
+
db.execute("begin")
|
55
|
+
res = db.execute("select master_lock from locks for update")
|
56
|
+
master = res.fetch[0]
|
57
|
+
res.finish
|
58
|
+
got_lock = false
|
59
|
+
if master == @master
|
60
|
+
got_lock = true
|
61
|
+
@master = "http://#{private_dns_name}/haproxy/monitor"
|
62
|
+
db.do("update locks set master_lock = '#{@master}'")
|
63
|
+
else
|
64
|
+
# new master, don't start monitoring till it comes up
|
65
|
+
@seen_good_check = false
|
66
|
+
@taking_over = false
|
67
|
+
@master = master
|
68
|
+
Log.write("Failed to grab lock, relenting: #{nodename}\nmaster is: #{@master}")
|
69
|
+
EM.add_timer(600) { Log.write "restarting monitoring"; setup_master_check_timer }
|
70
|
+
end
|
71
|
+
db.do("commit")
|
72
|
+
db.disconnect
|
73
|
+
got_lock
|
74
|
+
end
|
75
|
+
|
76
|
+
def start
|
77
|
+
setup_master_check_timer
|
78
|
+
EM.add_periodic_timer(300) { get_local_json }
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup_master_check_timer
|
82
|
+
cancel_master_check_timer
|
83
|
+
unless self_monitor_url == @master
|
84
|
+
@check_master_timer = EventMachine::PeriodicTimer.new(@opts[:heartbeat]) { check_master }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def cancel_master_check_timer
|
89
|
+
@check_master_timer && @check_master_timer.cancel
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_master
|
93
|
+
http = EventMachine::HttpRequest.new(@master).get :timeout => 10
|
94
|
+
|
95
|
+
http.callback {
|
96
|
+
unless http.response_header.status == 200
|
97
|
+
take_over_as_master
|
98
|
+
else
|
99
|
+
@seen_good_check = true
|
100
|
+
@bad_checks = 0
|
101
|
+
end
|
102
|
+
http.response_header.status
|
103
|
+
}
|
104
|
+
http.errback { |msg, err|
|
105
|
+
take_over_as_master
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def take_over_as_master
|
110
|
+
Log.write("Got a bad check: seen good check is #{@seen_good_check.inspect}")
|
111
|
+
@bad_checks += 1
|
112
|
+
if @bad_checks > 5 && @seen_good_check && !@taking_over
|
113
|
+
Log.write "I'm trying to take over!"
|
114
|
+
@taking_over = true
|
115
|
+
cancel_master_check_timer
|
116
|
+
if try_lock(private_dns_name)
|
117
|
+
Log.write("I got the lock!")
|
118
|
+
steal_ip
|
119
|
+
unless notify_awsm
|
120
|
+
timer = EventMachine::PeriodicTimer.new(5) { timer.cancel if notify_awsm }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def am_i_master?
|
127
|
+
res = false
|
128
|
+
@ec2.describe_addresses.each do |desc|
|
129
|
+
res = true if (desc[:public_ip] == public_ip && desc[:instance_id] == instance_id)
|
130
|
+
end
|
131
|
+
if res
|
132
|
+
db = get_mysql_handle
|
133
|
+
db.execute("begin")
|
134
|
+
rows = db.execute("select master_lock from locks for update")
|
135
|
+
master = rows.fetch[0]
|
136
|
+
rows.finish
|
137
|
+
db.do("update locks set master_lock = '#{self_monitor_url}'")
|
138
|
+
@master = self_monitor_url
|
139
|
+
cancel_master_check_timer
|
140
|
+
db.do("commit")
|
141
|
+
end
|
142
|
+
res
|
143
|
+
end
|
144
|
+
|
145
|
+
def notify_awsm
|
146
|
+
Log.write "Notifying awsm that I won"
|
147
|
+
res = call_api("promote_instance_to_master", :instance_id => instance_id)
|
148
|
+
case res['status']
|
149
|
+
when 'ok'
|
150
|
+
true
|
151
|
+
when 'already_promoted'
|
152
|
+
am_i_master?
|
153
|
+
EM.add_timer(600) { setup_master_check_timer unless am_i_master? }
|
154
|
+
true
|
155
|
+
else
|
156
|
+
false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def instance_id
|
161
|
+
@instance_id ||= open("http://169.254.169.254/latest/meta-data/instance-id").read
|
162
|
+
end
|
163
|
+
|
164
|
+
def private_dns_name
|
165
|
+
@private_dns_name ||= open("http://169.254.169.254/latest/meta-data/local-hostname").read
|
166
|
+
end
|
167
|
+
|
168
|
+
def self_monitor_url
|
169
|
+
"http://#{private_dns_name}/haproxy/monitor"
|
170
|
+
end
|
171
|
+
|
172
|
+
def steal_ip
|
173
|
+
if @ec2.disassociate_address(public_ip)
|
174
|
+
@ec2.associate_address(instance_id, public_ip)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def public_ip
|
179
|
+
@public_ip ||= @json['master_app_server']['public_ip']
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_local_json
|
183
|
+
@json = JSON.parse(IO.read("/etc/chef/dna.json"))
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_master_from_json
|
187
|
+
if host = @json['master_app_server']['private_dns_name']
|
188
|
+
@master = "http://#{host}/haproxy/monitor"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
data/spec/ey_api_spec.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) + "/../lib/"
|
2
|
+
require 'ey'
|
3
|
+
require 'fakeweb'
|
4
|
+
|
5
|
+
describe "EyApi" do
|
6
|
+
describe "#get_json" do
|
7
|
+
before(:each) do
|
8
|
+
@mock_environment_name = 'myenv'
|
9
|
+
|
10
|
+
FakeWeb.allow_net_connect = false # if it's not here, it doesn't exist
|
11
|
+
FakeWeb.register_uri(:post, "http://example.org/api/environments", :body => JSON.generate({
|
12
|
+
@mock_environment_name => {:id => 1}
|
13
|
+
}))
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
@api = Object.new
|
18
|
+
@api.instance_variable_set("@rest", RestClient::Resource.new('http://example.org'))
|
19
|
+
@api.instance_variable_set("@env", @mock_environment_name)
|
20
|
+
@api.instance_variable_set("@keys", {})
|
21
|
+
@api.extend(EyApi)
|
22
|
+
|
23
|
+
@api.stub!(:sleep)
|
24
|
+
end
|
25
|
+
|
26
|
+
after(:each) do
|
27
|
+
FakeWeb.clean_registry
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "when the JSON is available" do
|
31
|
+
before(:each) do
|
32
|
+
FakeWeb.register_uri(:post, "http://example.org/api/json_for_instance", :body => JSON.generate({:valid => "json"}))
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should pass through exceptions that it receives" do
|
36
|
+
JSON.stub!(:parse).and_raise(Exception)
|
37
|
+
lambda {
|
38
|
+
@api.get_json("i-decafbad")
|
39
|
+
}.should raise_error
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return a valid JSON structure" do
|
43
|
+
presumably_valid_json = @api.get_json("i-feedface")
|
44
|
+
lambda {
|
45
|
+
JSON[presumably_valid_json]
|
46
|
+
}.should_not raise_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "when the JSON is not yet available" do
|
51
|
+
|
52
|
+
it "should retry until the 503 goes away" do
|
53
|
+
FakeWeb.register_uri(:post, "http://example.org/api/json_for_instance", [{
|
54
|
+
:status => ["503", "Service Unavailable"],
|
55
|
+
:body => "{}",
|
56
|
+
}, {
|
57
|
+
:status => ["503", "Service Unavailable"],
|
58
|
+
:body => "{}",
|
59
|
+
}, {
|
60
|
+
:status => ["200", "OK"],
|
61
|
+
:body => JSON.generate({:valid => 'json'}),
|
62
|
+
}]
|
63
|
+
)
|
64
|
+
|
65
|
+
lambda {
|
66
|
+
JSON[@api.get_json("i-robot")]
|
67
|
+
}.should_not raise_error
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|