ey-flex-test 0.3.3
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/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
|
+
|