Skip to content

Commit 874e3a0

Browse files
author
Neil Greenwood
authored
Merge pull request #9 from neil-greenwood/update-ec2-cleanup-handlers
Update ec2 cleanup handlers
2 parents 630750e + 5db86d7 commit 874e3a0

File tree

2 files changed

+271
-77
lines changed

2 files changed

+271
-77
lines changed

handlers/other/ec2-cleanup.rb

Lines changed: 148 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
#!/usr/bin/env ruby
22
#
33
# CHANGELOG:
4-
# * 0.5.0: Initial Alert Logic fork
5-
# - Replaced fog with aws-sdk-core
6-
# - Uses IAM instead of keys
4+
# * 0.8.1:
5+
# - Alert Logic fork - manipulate client name to obtain EC2 instance ID
6+
# * 0.8.0:
7+
# - Added support to use ec2_region from client definition
8+
# * 0.7.0:
9+
# - Added method instance_id to check in client config section
10+
# - Update to new API event naming and simplifying ec2_node_should_be_deleted method and fixing
11+
# match that will work with any user state defined.
12+
# * 0.6.0:
13+
# - Fixed ec2_node_should_be_deleted to account for an empty insances array
14+
# * 0.5.0:
15+
# - Adds configuration to filter by state reason
716
# * 0.4.0:
817
# - Adds ability to specify a list of states an individual client can have in
918
# EC2. If none is specified, it filters out 'terminated' and 'stopped'
@@ -25,16 +34,76 @@
2534
# Optionally, you may specify a client attribute `ec2_states`, a list of valid
2635
# states an instance may have.
2736
#
37+
# You may also specify a client attribute `ec2_state_reasons`, a list of regular
38+
# expressions to match state reasons against. This is useful if you want to fail
39+
# on any `Client.*` state reason or on `Server.*` state reason. The default is
40+
# to match any state reason `.*` Regardless, eventually a client will be
41+
# deleted once AWS stops responding that the instance id exists.
42+
#
43+
# You could specify a ec2_states.json config file for the states like so:
44+
#
45+
# {
46+
# "ec2_node": {
47+
# "ec2_states": [
48+
# "terminated",
49+
# "stopping",
50+
# "shutting-down",
51+
# "stopped"
52+
# ]
53+
# }
54+
# }
55+
#
56+
# And add that to your /etc/sensu/conf.d directory.
57+
# If you do not specify any states the handler would not work
58+
#
2859
# NOTE: The implementation for correlating Sensu clients to EC2 instances may
2960
# need to be modified to fit your organization. The current implementation
3061
# assumes that Sensu clients' names are the same as their instance IDs in EC2.
3162
# If this is not the case, you can either sub-class this handler and override
32-
# `ec2_node_exists?` in your own organization-specific handler, or modify this
63+
# `ec2_node_should_be_deleted?` in your own organization-specific handler, or modify this
3364
# handler to suit your needs.
3465
#
35-
# Requires the following Rubygems (`gem install $GEM`):
36-
# - sensu-plugin
37-
# - aws-sdk-core
66+
#
67+
# A Sensu Client configuration using the ec2_region attribute:
68+
# {
69+
# "client": {
70+
# "name": "i-424242",
71+
# "address": "127.0.0.1",
72+
# "ec2_region": "eu-west-1",
73+
# "subscriptions": ["all"]
74+
# }
75+
# }
76+
# or embeded in the ec2 block
77+
# {
78+
# "client": {
79+
# "name": "i-424242",
80+
# "address": "127.0.0.1",
81+
# "ec2" : {
82+
# "region": "eu-west-1"
83+
# },
84+
# "subscriptions": ["all"]
85+
# }
86+
# }
87+
#
88+
# Or a Sensu Server configuration snippet:
89+
# {
90+
# "aws": {
91+
# "access_key": "adsafdafda",
92+
# "secret_key": "qwuieohajladsafhj23nm",
93+
# "region": "us-east-1c"
94+
# }
95+
# }
96+
#
97+
# Or you can set the following environment variables:
98+
# - AWS_ACCESS_KEY_ID
99+
# - AWS_SECRET_ACCESS_KEY
100+
# - EC2_REGION
101+
#
102+
# If none of the settings are found it will then attempt to
103+
# generate temporary credentials from the IAM instance profile
104+
#
105+
# If region is not specified in either of the above 3 mechanisms
106+
# we will make a request for the EC2 instances current region.
38107
#
39108
# To use, you can set it as the keepalive handler for a client:
40109
# {
@@ -57,7 +126,7 @@
57126
# "name": "keepalive",
58127
# "status": 2
59128
# },
60-
# "occurences": "eval: value > 2"
129+
# "occurrences": "eval: value > 2"
61130
# }
62131
# }
63132
# },
@@ -79,64 +148,105 @@
79148
# LICENSE for details
80149

81150
require 'timeout'
82-
require 'rubygems' if RUBY_VERSION < '1.9.0'
83151
require 'sensu-handler'
84-
require 'aws-sdk-core'
85-
require 'json'
86-
require 'open-uri'
152+
require 'net/http'
153+
require 'uri'
154+
require 'aws-sdk'
155+
#require 'sensu-plugins-aws'
87156

88157
class Ec2Node < Sensu::Handler
158+
#include Common
159+
89160
def filter; end
90161

162+
# Method handle
91163
def handle
92-
# #YELLOW
93-
unless ec2_node_exists? # rubocop:disable UnlessElse
164+
# Call ec2_node_should_be_deleted method and check for instance state and if valid delete from the sensu API otherwise
165+
# instance is in invalid state
166+
if ec2_node_should_be_deleted?
94167
delete_sensu_client!
95168
else
96-
puts "[EC2 Node] #{@event['client']['name']} exists in EC2"
169+
puts "[EC2 Node] #{instance_id} is in an invalid state"
97170
end
98171
end
99172

173+
# Method to delete client from sensu API
100174
def delete_sensu_client!
101175
response = api_request(:DELETE, '/clients/' + @event['client']['name']).code
102176
deletion_status(response)
103177
end
104178

105-
def ec2_node_exists?
106-
instance_ids = ec2.describe_instances(filters: [ { name: "instance-state-name", values: acquire_valid_states } ]).reservations.collect { |r| r.instances.map(&:instance_id) }.flatten
107-
# Strip the service name off so we can find it in EC2
108-
@event['client']['ec2-name'] = @event['client']['name'].scan(/-(i-\w+)$/)[0][0]
109-
instance_ids.each do |instance_id|
110-
return true if instance_id == @event['client']['ec2-name']
179+
def instance_id
180+
return @instance_id if @instance_id
181+
182+
scan_result = @event['client']['name'].scan(/-(i-\w+)$/)
183+
@instance_id =
184+
if scan_result[0]
185+
scan_result[0][0]
186+
else
187+
@event['client']['name']
188+
end
189+
@instance_id
190+
end
191+
192+
# Method to check if there is any insance and if instance is in a valid state that could be deleted
193+
def ec2_node_should_be_deleted?
194+
# Defining region for aws SDK object
195+
ec2 = Aws::EC2::Client.new(region: region)
196+
settings['ec2_node'] = {} unless settings['ec2_node']
197+
instance_states = @event['client']['ec2_states'] || settings['ec2_node']['ec2_states'] || ['shutting-down', 'terminated', 'stopping', 'stopped']
198+
instance_reasons = @event['client']['ec2_state_reasons'] || settings['ec2_node']['ec2_state_reasons'] || %w(Client.UserInitiatedShutdown Server.SpotInstanceTermination Client.InstanceInitiatedShutdown)
199+
200+
begin
201+
# Finding the instance
202+
instances = ec2.describe_instances(instance_ids: [instance_id]).reservations[0]
203+
# If instance is empty/nil instance id is not valid so client can be deleted
204+
if instances.nil? || instances.empty?
205+
true
206+
else
207+
# Checking for instance state and reason, and if matches any of the user defined or default reasons then
208+
# method returns True
209+
210+
# Returns instance state reason in AWS i.e: "Client.UserInitiatedShutdown"
211+
instance_state_reason = instances.instances[0].state_reason.nil? ? nil : instances.instances[0].state_reason.code
212+
# Returns the instance state i.e: "terminated"
213+
instance_state = instances.instances[0].state.name
214+
215+
# Return true is instance state and instance reason is valid
216+
instance_states.include?(instance_state) && instance_reasons.include?(instance_state_reason)
217+
end
218+
rescue Aws::EC2::Errors::InvalidInstanceIDNotFound
219+
true
111220
end
112-
false # no match found, node doesn't exist
113221
end
114222

115-
def ec2
116-
region = JSON.parse(open('http://169.254.169.254/latest/dynamic/instance-identity/document').read)['region']
117-
@ec2 ||= begin
118-
Aws::EC2::Client.new(region: region)
223+
def region
224+
@region ||= begin
225+
region_check = ENV['EC2_REGION']
226+
region_check = settings['aws']['region'] if settings.key?('aws')
227+
region_check = @event['client']['ec2_region'] if @event['client'].key?('ec2_region')
228+
region_check = @event['client']['ec2']['region'] if @event['client'].key?('ec2') && @event['client']['ec2'].key?('region')
229+
if region_check.nil? || region_check.empty?
230+
region_check = Net::HTTP.get(URI('http://169.254.169.254/latest/meta-data/placement/availability-zone'))
231+
matches = /(\w+\-\w+\-\d+)/.match(region_check)
232+
if !matches.nil? && !matches.captures.empty?
233+
region_check = matches.captures[0]
234+
end
235+
end
236+
region_check
119237
end
120238
end
121239

122240
def deletion_status(code)
123241
case code
124242
when '202'
125-
puts "[EC2 Node] 202: Successfully deleted Sensu client: #{@event['client']['name']}"
243+
puts "[EC2 Node] 202: Successfully deleted Sensu client: #{instance_id}"
126244
when '404'
127-
puts "[EC2 Node] 404: Unable to delete #{@event['client']['name']}}, doesn't exist!"
245+
puts "[EC2 Node] 404: Unable to delete #{instance_id}, doesn't exist!"
128246
when '500'
129-
puts "[EC2 Node] 500: Miscellaneous error when deleting #{@event['client']['name']}}"
130-
else
131-
puts "[EC2 Node] #{res}: Completely unsure of what happened!"
132-
end
133-
end
134-
135-
def acquire_valid_states
136-
if @event['client'].key?('ec2_states')
137-
return @event['client']['ec2_states']
247+
puts "[EC2 Node] 500: Miscellaneous error when deleting #{instance_id}"
138248
else
139-
return ['running']
249+
puts "[EC2 Node] #{code}: Completely unsure of what happened!"
140250
end
141251
end
142252
end

0 commit comments

Comments
 (0)