|
1 | 1 | #!/usr/bin/env ruby |
2 | 2 | # |
3 | 3 | # 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 |
7 | 16 | # * 0.4.0: |
8 | 17 | # - Adds ability to specify a list of states an individual client can have in |
9 | 18 | # EC2. If none is specified, it filters out 'terminated' and 'stopped' |
|
25 | 34 | # Optionally, you may specify a client attribute `ec2_states`, a list of valid |
26 | 35 | # states an instance may have. |
27 | 36 | # |
| 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 | +# |
28 | 59 | # NOTE: The implementation for correlating Sensu clients to EC2 instances may |
29 | 60 | # need to be modified to fit your organization. The current implementation |
30 | 61 | # assumes that Sensu clients' names are the same as their instance IDs in EC2. |
31 | 62 | # 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 |
33 | 64 | # handler to suit your needs. |
34 | 65 | # |
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. |
38 | 107 | # |
39 | 108 | # To use, you can set it as the keepalive handler for a client: |
40 | 109 | # { |
|
57 | 126 | # "name": "keepalive", |
58 | 127 | # "status": 2 |
59 | 128 | # }, |
60 | | -# "occurences": "eval: value > 2" |
| 129 | +# "occurrences": "eval: value > 2" |
61 | 130 | # } |
62 | 131 | # } |
63 | 132 | # }, |
|
79 | 148 | # LICENSE for details |
80 | 149 |
|
81 | 150 | require 'timeout' |
82 | | -require 'rubygems' if RUBY_VERSION < '1.9.0' |
83 | 151 | 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' |
87 | 156 |
|
88 | 157 | class Ec2Node < Sensu::Handler |
| 158 | + #include Common |
| 159 | + |
89 | 160 | def filter; end |
90 | 161 |
|
| 162 | + # Method handle |
91 | 163 | 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? |
94 | 167 | delete_sensu_client! |
95 | 168 | else |
96 | | - puts "[EC2 Node] #{@event['client']['name']} exists in EC2" |
| 169 | + puts "[EC2 Node] #{instance_id} is in an invalid state" |
97 | 170 | end |
98 | 171 | end |
99 | 172 |
|
| 173 | + # Method to delete client from sensu API |
100 | 174 | def delete_sensu_client! |
101 | 175 | response = api_request(:DELETE, '/clients/' + @event['client']['name']).code |
102 | 176 | deletion_status(response) |
103 | 177 | end |
104 | 178 |
|
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 |
111 | 220 | end |
112 | | - false # no match found, node doesn't exist |
113 | 221 | end |
114 | 222 |
|
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 |
119 | 237 | end |
120 | 238 | end |
121 | 239 |
|
122 | 240 | def deletion_status(code) |
123 | 241 | case code |
124 | 242 | 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}" |
126 | 244 | 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!" |
128 | 246 | 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}" |
138 | 248 | else |
139 | | - return ['running'] |
| 249 | + puts "[EC2 Node] #{code}: Completely unsure of what happened!" |
140 | 250 | end |
141 | 251 | end |
142 | 252 | end |
0 commit comments