ey-flex-test 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
+
@@ -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
+