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