qurd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +367 -0
- data/Rakefile +9 -0
- data/bin/qurd +10 -0
- data/lib/hash.rb +6 -0
- data/lib/qurd.rb +49 -0
- data/lib/qurd/action.rb +92 -0
- data/lib/qurd/action/chef.rb +128 -0
- data/lib/qurd/action/dummy.rb +27 -0
- data/lib/qurd/action/route53.rb +168 -0
- data/lib/qurd/configuration.rb +182 -0
- data/lib/qurd/listener.rb +207 -0
- data/lib/qurd/message.rb +231 -0
- data/lib/qurd/mixins.rb +8 -0
- data/lib/qurd/mixins/aws_clients.rb +37 -0
- data/lib/qurd/mixins/configuration.rb +29 -0
- data/lib/qurd/mixins/configuration_helpers.rb +79 -0
- data/lib/qurd/processor.rb +97 -0
- data/lib/qurd/version.rb +5 -0
- data/lib/string.rb +12 -0
- data/qurd.gemspec +32 -0
- data/test/action_test.rb +115 -0
- data/test/chef_test.rb +206 -0
- data/test/configuration_test.rb +333 -0
- data/test/dummy_action_test.rb +51 -0
- data/test/inputs/foo.pem +27 -0
- data/test/inputs/knife.rb +9 -0
- data/test/inputs/qurd.yml +32 -0
- data/test/inputs/qurd_chef.yml +35 -0
- data/test/inputs/qurd_chef_route53.yml +43 -0
- data/test/inputs/qurd_route53.yml +39 -0
- data/test/inputs/qurd_route53_wrong.yml +37 -0
- data/test/inputs/validator.pem +27 -0
- data/test/listener_test.rb +135 -0
- data/test/message_test.rb +187 -0
- data/test/mixin_aws_clients_test.rb +28 -0
- data/test/mixin_configuration_test.rb +36 -0
- data/test/processor_test.rb +41 -0
- data/test/responses/aws/ec2-describe-instances-0.xml +2 -0
- data/test/responses/aws/ec2-describe-instances-1.xml +127 -0
- data/test/responses/aws/error-response.xml +1 -0
- data/test/responses/aws/route53-change-resource-record-sets.xml +2 -0
- data/test/responses/aws/route53-list-hosted-zones-by-name-0.xml +3 -0
- data/test/responses/aws/route53-list-hosted-zones-by-name-1.xml +4 -0
- data/test/responses/aws/route53-list-hosted-zones-by-name-n.xml +5 -0
- data/test/responses/aws/route53-list-resource-record-sets-0.xml +2 -0
- data/test/responses/aws/route53-list-resource-record-sets-1.xml +4 -0
- data/test/responses/aws/route53-list-resource-record-sets-n.xml +6 -0
- data/test/responses/aws/sqs-list-queues-0.xml +1 -0
- data/test/responses/aws/sqs-list-queues-n.xml +4 -0
- data/test/responses/aws/sqs-receive-message-1-launch.xml +6 -0
- data/test/responses/aws/sqs-receive-message-1-launch_error.xml +6 -0
- data/test/responses/aws/sqs-receive-message-1-other.xml +12 -0
- data/test/responses/aws/sqs-receive-message-1-terminate.xml +6 -0
- data/test/responses/aws/sqs-receive-message-1-terminate_error.xml +6 -0
- data/test/responses/aws/sqs-receive-message-1-test.xml +12 -0
- data/test/responses/aws/sqs-set-queue-attributes.xml +1 -0
- data/test/responses/aws/sts-assume-role.xml +17 -0
- data/test/responses/chef/search-client-name-0.json +6 -0
- data/test/responses/chef/search-client-name-1.json +7 -0
- data/test/responses/chef/search-client-name-n.json +8 -0
- data/test/responses/chef/search-node-instance-0.json +5 -0
- data/test/responses/chef/search-node-instance-1.json +784 -0
- data/test/responses/chef/search-node-instance-n.json +1565 -0
- data/test/responses/ec2/latest-meta-data-iam-security-credentials-client.txt +9 -0
- data/test/responses/ec2/latest-meta-data-iam-security-credentials.txt +1 -0
- data/test/route53_test.rb +231 -0
- data/test/support/web_mock_stubs.rb +109 -0
- data/test/test_helper.rb +10 -0
- metadata +307 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
# rubocop:disable ClassLength
|
2
|
+
module Qurd
|
3
|
+
# Parse a configuration file, create a logger and various data structures
|
4
|
+
class Configuration
|
5
|
+
include Singleton
|
6
|
+
include Mixins::ConfigurationHelpers
|
7
|
+
# @!attribute [r] config
|
8
|
+
# Configuration options, ie
|
9
|
+
# +aws_credentials+, +auto_scaling_queues+, +actions+, +daemonize+,
|
10
|
+
# +dry_run+, +listen_timeout+, +log_file+, +log_level+, +pid_file+,
|
11
|
+
# +save_failures+, +sqs_set_attributes_timeout+, +visibility_timeout+,
|
12
|
+
# +wait_time+.
|
13
|
+
# Additional configuration keys include +listeners+.
|
14
|
+
# @return [Hashie::Mash] the config YAML as a Mash
|
15
|
+
# @!attribute [r] logger
|
16
|
+
# The logger
|
17
|
+
# @return [Cabin::Channel]
|
18
|
+
attr_reader :config, :logger
|
19
|
+
|
20
|
+
# Initialize Qurd
|
21
|
+
# @param [String] config_path The path to the config file, default
|
22
|
+
# +/etc/qurd/config.yml+
|
23
|
+
def init(config_path)
|
24
|
+
config_path ||= '/etc/qurd/config.yml'
|
25
|
+
@config = Hashie::Mash.new YAML.load(File.read(config_path))
|
26
|
+
@sqs_queues = {}
|
27
|
+
@config.daemonize = false if @config.daemonize.nil?
|
28
|
+
@config.dry_run = get_or_default(@config, :dry_run, false)
|
29
|
+
@config.pid_file ||= '/var/run/qurd/qurd.pid'
|
30
|
+
@config.save_failures = get_or_default(@config, :save_failures, true)
|
31
|
+
@queues = []
|
32
|
+
@aws_credentials = []
|
33
|
+
vt = get_or_default(@config, :visibility_timeout, 300, :to_s)
|
34
|
+
wt = get_or_default(@config, :wait_time, 20, :to_s)
|
35
|
+
st = get_or_default(@config, :sqs_set_attributes_timeout, 10, :to_f)
|
36
|
+
lt = get_or_default(@config, :listen_timeout, vt, :to_f)
|
37
|
+
@config.visibility_timeout = vt
|
38
|
+
@config.wait_time = wt
|
39
|
+
@config.sqs_set_attributes_timeout = st
|
40
|
+
@config.listen_timeout = lt
|
41
|
+
%w[launch launch_error terminate terminate_error test].each do |action|
|
42
|
+
@config.actions[action] ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
configure_logger
|
46
|
+
end
|
47
|
+
|
48
|
+
# Configure Qurd
|
49
|
+
# @param [String] config_path The path to the config file, default
|
50
|
+
# +/etc/qurd/config.yml+
|
51
|
+
def configure(config_path)
|
52
|
+
init(config_path)
|
53
|
+
mkdir_p_file!(@config.pid_file)
|
54
|
+
|
55
|
+
configure_credentials
|
56
|
+
configure_auto_scaling_queues
|
57
|
+
configure_actions
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determine if the daemon is running in debug mode
|
61
|
+
# @return [Boolean]
|
62
|
+
def debug?
|
63
|
+
config.log_level == 'debug'
|
64
|
+
end
|
65
|
+
|
66
|
+
# Log an error and raise an exception
|
67
|
+
# @param [String] msg The error and exception message
|
68
|
+
# @param [Exception] e The exception to raise
|
69
|
+
def logger!(msg, e = RuntimeError)
|
70
|
+
logger.error msg
|
71
|
+
fail e, msg
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get a logging context and optionally initialize it
|
75
|
+
# @param [Hash] attrs a hash of values to +merge+ into the context
|
76
|
+
# @return [Cabin::Context]
|
77
|
+
def get_context(attrs = {})
|
78
|
+
ctx = logger.context
|
79
|
+
attrs.each do |k, v|
|
80
|
+
ctx[k] = v
|
81
|
+
end
|
82
|
+
ctx
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def configure_credentials
|
88
|
+
if config.aws_credentials.nil? || config.aws_credentials.empty?
|
89
|
+
creds = default_credentials
|
90
|
+
else
|
91
|
+
creds = config.aws_credentials.map do |cred|
|
92
|
+
cred.options ||= {}
|
93
|
+
case cred.type
|
94
|
+
when 'assume_role_credentials'
|
95
|
+
assume_role_credentials(cred)
|
96
|
+
when 'credentials'
|
97
|
+
credentials(cred)
|
98
|
+
when 'shared_credentials'
|
99
|
+
shared_credentials(cred)
|
100
|
+
when 'instance_profile_credentials'
|
101
|
+
instance_profile_credentials(cred)
|
102
|
+
else qurd_logger! "Credential type unknown: '#{cred.type}'"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
config.aws_credentials = Hash[creds]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Convert strings to objects
|
110
|
+
def configure_actions
|
111
|
+
missing = config.actions.inject([]) do |ary, mod|
|
112
|
+
action, klasses = mod
|
113
|
+
return ary if klasses.nil?
|
114
|
+
ctx = get_context(action: action)
|
115
|
+
logger! 'Action types must be an array' unless klasses.is_a?(Array)
|
116
|
+
klasses.map! do |klass|
|
117
|
+
begin
|
118
|
+
k = string2class(klass)
|
119
|
+
k.configure(action)
|
120
|
+
k
|
121
|
+
rescue NameError, LoadError => e
|
122
|
+
logger.error(e)
|
123
|
+
ary << klass
|
124
|
+
end
|
125
|
+
end
|
126
|
+
ctx.clear
|
127
|
+
ary
|
128
|
+
end
|
129
|
+
|
130
|
+
m = missing.uniq.join(', ')
|
131
|
+
logger! "Class undefined for actions: #{m}" if missing.any?
|
132
|
+
end
|
133
|
+
|
134
|
+
# Configure Cabin and Aws logging
|
135
|
+
def configure_logger
|
136
|
+
@logger = Cabin::Channel.new
|
137
|
+
if config.log_file || config.daemonize
|
138
|
+
path = config.log_file || '/var/log/qurd/qurd.log'
|
139
|
+
mkdir_p_file!(path)
|
140
|
+
config.log_file_io = open(path, 'w')
|
141
|
+
@ruby_logger = Logger.new(config.log_file_io)
|
142
|
+
@logger.level = (config.log_level || :info).to_sym
|
143
|
+
else
|
144
|
+
@logger.level = (config.log_level || :debug).to_sym
|
145
|
+
@ruby_logger = Logger.new(STDOUT)
|
146
|
+
end
|
147
|
+
@logger.subscribe(@ruby_logger)
|
148
|
+
|
149
|
+
Aws.config[:logger] = @ruby_logger
|
150
|
+
Aws.config[:http_wire_trace] = debug?
|
151
|
+
|
152
|
+
@logger.debug('Logging configured')
|
153
|
+
end
|
154
|
+
|
155
|
+
# Configure sqs clients and queues
|
156
|
+
def configure_auto_scaling_queues
|
157
|
+
config.listeners = config.auto_scaling_queues.map do |name, monitor|
|
158
|
+
if (config.auto_scaling_queues.nil? ||
|
159
|
+
config.auto_scaling_queues.empty?) &&
|
160
|
+
config.aws_credentials.default
|
161
|
+
creds = config.aws_credentials.default
|
162
|
+
monitor.credentials = 'default'
|
163
|
+
else
|
164
|
+
creds = config.aws_credentials[monitor.credentials]
|
165
|
+
end
|
166
|
+
verify_account!(name, monitor)
|
167
|
+
logger!("Undefined credential: '#{monitor.credentials}'") unless creds
|
168
|
+
vt = get_or_default(monitor, :visibility_timeout,
|
169
|
+
config.visibility_timeout, :to_s)
|
170
|
+
wt = get_or_default(monitor, :wait_time, config.wait_time, :to_s)
|
171
|
+
Listener.new(
|
172
|
+
aws_credentials: creds,
|
173
|
+
name: name,
|
174
|
+
queues: monitor.queues,
|
175
|
+
region: monitor.region,
|
176
|
+
visibility_timeout: vt,
|
177
|
+
wait_time: wt
|
178
|
+
)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# rubocop:disable ClassLength
|
2
|
+
# Gem module
|
3
|
+
module Qurd
|
4
|
+
# Provide an interface for interacting with configured queues and AWS.
|
5
|
+
class Listener
|
6
|
+
include Qurd::Mixins::AwsClients
|
7
|
+
include Qurd::Mixins::Configuration
|
8
|
+
|
9
|
+
# @!attribute aws_credentials [r]
|
10
|
+
# The AWS credentials for the account
|
11
|
+
# @return [Aws::Credentials]
|
12
|
+
# @!attribute message [r]
|
13
|
+
# The message reveived
|
14
|
+
# @return [Qurd::Message]
|
15
|
+
# @!attribute name [r]
|
16
|
+
# The name of the executor
|
17
|
+
# @return [String]
|
18
|
+
# @!attribute queues [r]
|
19
|
+
# An array of AWS SQS URLs for the account
|
20
|
+
# @return [Array<String>]
|
21
|
+
# @!attribute region [r]
|
22
|
+
# The AWS region for the account
|
23
|
+
# @return [String]
|
24
|
+
# @!attribute visibility_timeout [r]
|
25
|
+
# @return [String]
|
26
|
+
# @!attribute wait_time [r]
|
27
|
+
# @return [String]
|
28
|
+
attr_reader :aws_credentials,
|
29
|
+
:message,
|
30
|
+
:name,
|
31
|
+
:queues,
|
32
|
+
:region,
|
33
|
+
:visibility_timeout,
|
34
|
+
:wait_time
|
35
|
+
|
36
|
+
# @param [Hash] attrs
|
37
|
+
# @option attrs [Aws::Credentials] :aws_credentials
|
38
|
+
# @option attrs [String] :name
|
39
|
+
# @option attrs [Array<String|Regexp>] :queues An array of SQS names
|
40
|
+
# and Regexps
|
41
|
+
# @option attrs [String] :region
|
42
|
+
# @option attrs [String] :visibility_timeout
|
43
|
+
# @option attrs [String] :wait_time
|
44
|
+
def initialize(attrs = {})
|
45
|
+
@aws_credentials = attrs[:aws_credentials]
|
46
|
+
@name = attrs[:name]
|
47
|
+
@region = attrs[:region]
|
48
|
+
@visibility_timeout = attrs[:visibility_timeout]
|
49
|
+
@wait_time = attrs[:wait_time]
|
50
|
+
@queues = convert_queues attrs[:queues]
|
51
|
+
configure_queues
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create a thread for each queue URL, a context denoting the listener name
|
55
|
+
# and the queue URL.
|
56
|
+
# @param [Proc] _block the proc each thread should run
|
57
|
+
# @yieldparam [String] url the url of the queue
|
58
|
+
# @yieldparam [Cabin::Context] ctx the logging context
|
59
|
+
def queue_threads(&_block)
|
60
|
+
queues.map do |qurl|
|
61
|
+
qurd_logger.debug("Creating thread for #{qurl}")
|
62
|
+
Thread.new(qurl) do |url|
|
63
|
+
ctx = qurd_config.get_context(name: name, queue_name: url[/([^\/]+)$/])
|
64
|
+
qurd_logger.debug('Thread running')
|
65
|
+
yield url, ctx
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create one thread per +queue+, receive messages from it and process each
|
71
|
+
# message received
|
72
|
+
# @return [Array<Thread>]
|
73
|
+
def listen
|
74
|
+
queue_threads do |qurl, _context|
|
75
|
+
loop do
|
76
|
+
begin
|
77
|
+
msgs = aws_client(:SQS).receive_message(
|
78
|
+
queue_url: qurl,
|
79
|
+
wait_time_seconds: wait_time,
|
80
|
+
visibility_timeout: visibility_timeout
|
81
|
+
)
|
82
|
+
threads = process_messages(qurl, msgs)
|
83
|
+
joins = threads.map do |thread|
|
84
|
+
thread.join(qurd_configuration.listen_timeout)
|
85
|
+
end
|
86
|
+
if joins.compact.count != threads.count
|
87
|
+
qurd_logger.warn('Some threads timed out')
|
88
|
+
end
|
89
|
+
rescue Aws::Errors::ServiceError => e
|
90
|
+
qurd_logger.error("Aws raised #{e}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# @private
|
97
|
+
def inspect
|
98
|
+
format('<Qurd::Listener:%x name:%s>', object_id, name)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def process_messages(qurl, msgs)
|
104
|
+
msgs.messages.map do |msg|
|
105
|
+
Thread.new(msg) do |m|
|
106
|
+
qurd_logger.debug("Found message #{msg}")
|
107
|
+
r = Processor.new self, m, name, qurl
|
108
|
+
r.process
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def configure_queues
|
114
|
+
threads = configure_queues_threads
|
115
|
+
joins = threads.map do |thread|
|
116
|
+
thread.join(qurd_configuration.sqs_set_attributes_timeout)
|
117
|
+
end
|
118
|
+
|
119
|
+
qurd_logger! 'One or more threads timed out' \
|
120
|
+
if joins.compact.count != threads.count
|
121
|
+
end
|
122
|
+
|
123
|
+
def configure_queues_threads
|
124
|
+
queue_threads do |q, _context|
|
125
|
+
qurd_logger.debug("Setting wait_time:#{wait_time} " \
|
126
|
+
"visibility_timeout:#{visibility_timeout} #{q}")
|
127
|
+
begin
|
128
|
+
aws_client(:SQS).set_queue_attributes(
|
129
|
+
queue_url: q,
|
130
|
+
attributes: {
|
131
|
+
ReceiveMessageWaitTimeSeconds: wait_time,
|
132
|
+
VisibilityTimeout: visibility_timeout
|
133
|
+
}
|
134
|
+
)
|
135
|
+
rescue Aws::SQS::Errors::ServiceError::QueueDoesNotExist => e
|
136
|
+
qurd_logger.error("SQS raised #{e}")
|
137
|
+
rescue Aws::SQS::Errors::ServiceError => e
|
138
|
+
qurd_logger.error("SQS raised #{e}")
|
139
|
+
raise e
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Convert a regex string to a regex, including modifiers
|
145
|
+
# @param [String] r String form of the regex
|
146
|
+
# @return [Regexp] The compiled regex
|
147
|
+
# @example With modifier
|
148
|
+
# Qurd::Configuration.parse_regex("/foo/i")
|
149
|
+
def parse_regex(r)
|
150
|
+
# /foo/ or /foo/i
|
151
|
+
m = r.match %r{\A/(.*)/([a-z]*)\Z}mx
|
152
|
+
qurd_logger.debug("Found re: #{m[0]} 1: #{m[1]} 2: #{m[2]}")
|
153
|
+
args = modifier2int(m[2])
|
154
|
+
regex = Regexp.new(m[1], args)
|
155
|
+
qurd_logger.debug("Compiled regex #{regex}")
|
156
|
+
queue_url regex
|
157
|
+
end
|
158
|
+
|
159
|
+
def modifier2int(str)
|
160
|
+
args = 0
|
161
|
+
str.each_byte do |c|
|
162
|
+
args |= case c.chr
|
163
|
+
when 'i' then Regexp::IGNORECASE
|
164
|
+
when 'm' then Regexp::MULTILINE
|
165
|
+
when 'x' then Regexp::EXTENDED
|
166
|
+
when 'o' then 0
|
167
|
+
when 'e' then 16
|
168
|
+
else qurd_logger! "Unknown regex modifier #{c.chr}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
args
|
172
|
+
end
|
173
|
+
|
174
|
+
# Find the SQS URL for a named queue or a regex
|
175
|
+
# @overload queue_url(name)
|
176
|
+
# @param [String] name The AWS SQS name
|
177
|
+
# @return [String] AWS SQS URL
|
178
|
+
#
|
179
|
+
# @overload queue_url(name)
|
180
|
+
# @param [Regexp] name regex of a queue name
|
181
|
+
# @return [Array<String>]
|
182
|
+
def queue_url(name)
|
183
|
+
@sqs_queues ||= aws_client(:SQS).list_queues.queue_urls
|
184
|
+
|
185
|
+
if name.respond_to?(:upcase)
|
186
|
+
url = @sqs_queues.find { |u| u[/([^\/]+$)/] == name }
|
187
|
+
else
|
188
|
+
url = @sqs_queues.select { |u| u =~ name }
|
189
|
+
end
|
190
|
+
qurd_logger.debug("Queue #{name} found '#{url}'")
|
191
|
+
qurd_logger.warn("No queue found for '#{name}'") if url.nil? || url.empty?
|
192
|
+
url
|
193
|
+
rescue Aws::SQS::Errors::ServiceError => e
|
194
|
+
qurd_logger.error("SQS raised #{e}")
|
195
|
+
raise e
|
196
|
+
end
|
197
|
+
|
198
|
+
# Convert regexes to and strings to queue URLs
|
199
|
+
# @param [Array<String>] queues An array of queues to monitor
|
200
|
+
# @return [Array<String>] SQS URLs
|
201
|
+
def convert_queues(queues)
|
202
|
+
queues.map do |q|
|
203
|
+
q[0] == '/' ? parse_regex(q) : queue_url(q)
|
204
|
+
end.flatten.compact
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/qurd/message.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
# rubocop:disable Metrics/LineLength
|
2
|
+
module Qurd
|
3
|
+
# Convert an SQS auto scaling message to a more usable object
|
4
|
+
# @example SQS auto scaling message
|
5
|
+
# {
|
6
|
+
# "Type" : "Notification",
|
7
|
+
# "MessageId" : "e4379a5a-e119-53f7-b6ef-d7dbd32d31fe",
|
8
|
+
# "TopicArn" : "arn:aws:sns:us-east-1:123456890:test-ScalingNotificationsTopic-HPPYDAYSAGAIN",
|
9
|
+
# "Subject" : "Auto Scaling: termination for group \"test2-AutoScalingGroup-1QDX3CNO5SU3D\"",
|
10
|
+
# "Message" : "{\"StatusCode\":\"InProgress\",\"Service\":\"AWS Auto Scaling\",\"AutoScalingGroupName\":\"test2-AutoScalingGroup-1QDX3CNO5SU3D\",\"Description\":\"Terminating EC2 instance: i-08e58cf8\",\"ActivityId\":\"93faaf3a-28cb-4982-a690-0a73c989ab1f\",\"Event\":\"autoscaling:EC2_INSTANCE_TERMINATE\",\"Details\":{\"Availability Zone\":\"us-east-1a\",\"Subnet ID\":\"subnet-3c3e0e14\"},\"AutoScalingGroupARN\":\"arn:aws:autoscaling:us-east-1:123456890:autoScalingGroup:4edb2535-5015-4b81-b668-88ecb0effcb7:autoScalingGroupName/test2-AutoScalingGroup-1QDX3CNO5SU3D\",\"Progress\":50,\"Time\":\"2015-03-16T19:33:08.181Z\",\"AccountId\":\"123456890\",\"RequestId\":\"93faaf3a-28cb-4982-a690-0a73c989ab1f\",\"StatusMessage\":\"\",\"EndTime\":\"2015-03-16T19:33:08.181Z\",\"EC2InstanceId\":\"i-08e58cf8\",\"StartTime\":\"2015-03-16T19:29:14.911Z\",\"Cause\":\"At 2015-03-16T19:29:14Z an instance was taken out of service in response to a ELB system health check failure.\"}",
|
11
|
+
# "Timestamp" : "2015-03-16T19:33:08.242Z",
|
12
|
+
# "SignatureVersion" : "1",
|
13
|
+
# "Signature" : "I+SE8tMiq13/wDTPTJnJvHYi3jSjChhYByJAsnhY0wGa+0lxXc18vPIn9hIT0tYRNWMcR/Xn1AUNsgHrLjzB93xukyKA2CDff08zIuP0l4Xle/FSEJzfkJ0FDqZnzelFuZ2PMtO3lf5UY7CWZg/wKJv6I9CNJF4Ll9YgvC8Moe/31VwJwNy4TRAWdBhDuRXLjbEHoFNGjaGquiduOGySrgRmm74d0P0zWj7IfWbqO6ReNG2ADrqw+Bhn6dAkkeFH+9vJZeKdUCgsXX8XCBHcWX+yAb4WJH90hdosLN12DCdn2AvNgQfoTdpDPkTHC+QcwfRs52d3MD2WLrUfBMBy0A==",
|
14
|
+
# "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem",
|
15
|
+
# "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456890:test-ScalingNotificationsTopic-HPPYDAYSAGAIN:bd850bb2-1a69-4456-a517-c645a26f54b2"
|
16
|
+
# }
|
17
|
+
class Message
|
18
|
+
extend Qurd::Mixins::Configuration
|
19
|
+
|
20
|
+
# Add setter and getter instances methods. If the get or set method is
|
21
|
+
# already defined, an exception will be raised.
|
22
|
+
# @param [Symbol] name the name of the method to add
|
23
|
+
def self.add_accessor(name)
|
24
|
+
if instance_methods.include?(name) ||
|
25
|
+
instance_methods.include?("#{name}=")
|
26
|
+
qurd_logger.warn "Can not replace a method! (#{name})"
|
27
|
+
end
|
28
|
+
attr_accessor name
|
29
|
+
end
|
30
|
+
|
31
|
+
include Qurd::Mixins::Configuration
|
32
|
+
include Qurd::Mixins::AwsClients
|
33
|
+
# @!attribute aws_credentials [r]
|
34
|
+
# @return [Aws::Credentials]
|
35
|
+
# @!attribute context [r]
|
36
|
+
# Cabin::Channel logs messages as well as context. Context is retained
|
37
|
+
# until it is cleared.
|
38
|
+
# @return [Cabin::Context] Context data
|
39
|
+
# @!attribute exceptions [r]
|
40
|
+
# Action exceptions
|
41
|
+
# @return [Array<Exception>]
|
42
|
+
# @!attribute name [r]
|
43
|
+
# The Listener name
|
44
|
+
# @return [String]
|
45
|
+
# @!attribute queue_url [r]
|
46
|
+
# The SQS url the message came from
|
47
|
+
# @return [String]
|
48
|
+
# @!attribute region [r]
|
49
|
+
# AWS region
|
50
|
+
# @return [String]
|
51
|
+
attr_reader :aws_credentials,
|
52
|
+
:context,
|
53
|
+
:exceptions,
|
54
|
+
:name,
|
55
|
+
:queue_url,
|
56
|
+
:region
|
57
|
+
|
58
|
+
#
|
59
|
+
# @param [Hash] attrs
|
60
|
+
# @option attrs [Aws::Credentials] :aws_credentials
|
61
|
+
# @option attrs [String] :name
|
62
|
+
# @option attrs [String] :queue_url
|
63
|
+
# @option attrs [String] :region msg AWS SQS message
|
64
|
+
# @option attrs [Struct] :message AWS SQS message
|
65
|
+
def initialize(attrs)
|
66
|
+
@aws_credentials = attrs[:aws_credentials]
|
67
|
+
@name = attrs[:name]
|
68
|
+
@queue_url = attrs[:queue_url]
|
69
|
+
@region = attrs[:region]
|
70
|
+
@sqs_message = attrs[:message]
|
71
|
+
|
72
|
+
@exceptions = []
|
73
|
+
@failed = false
|
74
|
+
@context = qurd_config.get_context(name: @name,
|
75
|
+
queue_name: (@queue_url[/[^\/]+$/] rescue nil),
|
76
|
+
instance_id: instance_id,
|
77
|
+
message_id: message_id,
|
78
|
+
action: action)
|
79
|
+
|
80
|
+
qurd_logger.info "Received #{body.Subject} Cause #{message.Cause} Event #{message.Event}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Convert the SQS message +body+ to a mash, keys include +Type+, +MessageId+,
|
84
|
+
# +TopicArn+, +Subject+, +Message+, +Timestamp+, +SignatureVersion+,
|
85
|
+
# +Signature+, +SigningCertURL+, +UnsubscribeURL+
|
86
|
+
# @return [Hashie::Mash]
|
87
|
+
def body
|
88
|
+
@body ||= Hashie::Mash.new JSON.load(@sqs_message.body)
|
89
|
+
rescue JSON::ParserError
|
90
|
+
@body = Hashie::Mash.new {}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Convert +body.Message+ to a mash, keys include
|
94
|
+
# +StatusCode+, +Service+, +AutoScalingGroupName+, +Description+,
|
95
|
+
# +ActivityId+, +Event+, +Details+ +AutoScalingGroupARN+ +Progress+ +Time+
|
96
|
+
# +AccountId+ +RequestId+, +StatusMessage+ +EndTime+ +EC2InstanceId+
|
97
|
+
# +StartTime+ +Cause+
|
98
|
+
# @return [Hashie::Mash]
|
99
|
+
def message
|
100
|
+
@message ||= Hashie::Mash.new JSON.load(body.Message)
|
101
|
+
rescue JSON::ParserError
|
102
|
+
@message = Hashie::Mash.new {}
|
103
|
+
end
|
104
|
+
|
105
|
+
# The SQS message's +EC2InstanceId+
|
106
|
+
# @return [String]
|
107
|
+
def instance_id
|
108
|
+
@instance_id ||= message.EC2InstanceId
|
109
|
+
end
|
110
|
+
|
111
|
+
# The +body.MessageId+
|
112
|
+
# @return [String]
|
113
|
+
def message_id
|
114
|
+
@message_id ||= body.MessageId
|
115
|
+
end
|
116
|
+
|
117
|
+
# The SQS +receipt_handle+, used to delete a message
|
118
|
+
# @return [String]
|
119
|
+
def receipt_handle
|
120
|
+
@sqs_message.receipt_handle
|
121
|
+
end
|
122
|
+
|
123
|
+
# Record an action failure
|
124
|
+
# @param [Exception] e The exception
|
125
|
+
def failed!(e = nil)
|
126
|
+
qurd_logger.debug 'Failed'
|
127
|
+
@exceptions << e if e
|
128
|
+
@failed = true
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# Has processing the message failed
|
133
|
+
# @return [Boolean]
|
134
|
+
def failed?
|
135
|
+
@failed == true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Memozied EC2 instance. Caller must anticipate +nil+ results, as instances
|
139
|
+
# may terminate before the message is received.
|
140
|
+
# @param [Fixnum] tries The number of times to retry the Aws API
|
141
|
+
# @return [Struct|nil]
|
142
|
+
def instance(tries = nil)
|
143
|
+
return @instance if @instance
|
144
|
+
@instance = aws_instance(tries)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Memoize the instance's +Name+ tag
|
148
|
+
def instance_name
|
149
|
+
return @instance_name if @instance_name
|
150
|
+
@instance_name = instance.tags.find do |t|
|
151
|
+
t.key == 'Name'
|
152
|
+
end.value
|
153
|
+
qurd_logger.debug("Found instance name '#{@instance_name}'")
|
154
|
+
@instance_name
|
155
|
+
rescue NoMethodError
|
156
|
+
qurd_logger.debug('No instance found')
|
157
|
+
@instance_name = nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Convert the +message.Event+ to an action
|
161
|
+
# @return [String] +launch+, +launch_error+, +terminate+, +terminate_error+,
|
162
|
+
# or +test+
|
163
|
+
def action
|
164
|
+
case message.Event
|
165
|
+
when 'autoscaling:EC2_INSTANCE_LAUNCH' then 'launch'
|
166
|
+
when 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' then 'launch_error'
|
167
|
+
when 'autoscaling:EC2_INSTANCE_TERMINATE' then 'terminate'
|
168
|
+
when 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' then 'terminate_error'
|
169
|
+
when 'autoscaling:TEST_NOTIFICATION' then 'test'
|
170
|
+
else
|
171
|
+
qurd_logger.info "Ignoring #{message.Event}"
|
172
|
+
failed!
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Delete an AWS SQS message
|
177
|
+
def delete
|
178
|
+
qurd_logger.debug('Preparing to delete message')
|
179
|
+
if failed? && qurd_configuration.save_failures
|
180
|
+
qurd_logger.error 'Message failed processing, not deleting'
|
181
|
+
elsif qurd_configuration.dry_run
|
182
|
+
qurd_logger.info 'Dry run'
|
183
|
+
else
|
184
|
+
delete_message
|
185
|
+
end
|
186
|
+
context.clear
|
187
|
+
end
|
188
|
+
|
189
|
+
# @private
|
190
|
+
def inspect
|
191
|
+
format('#<Qurd::Message message_id:%s subject:%s cause:%s ' \
|
192
|
+
'instance_id:%s instance:%s>',
|
193
|
+
message_id,
|
194
|
+
body.Subject,
|
195
|
+
message.Cause,
|
196
|
+
instance_id,
|
197
|
+
instance)
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
# Get the Aws EC2 instance, using +instance_id+
|
203
|
+
# @param [Fixnum] tries the number of retries
|
204
|
+
# @return [Struct|nil]
|
205
|
+
# @see instance_id
|
206
|
+
# @see instance
|
207
|
+
def aws_instance(tries = nil)
|
208
|
+
return unless instance_id
|
209
|
+
aws_retryable(tries) do
|
210
|
+
aws_client(:EC2).describe_instances(
|
211
|
+
instance_ids: [instance_id]
|
212
|
+
).reservations.first.instances.first
|
213
|
+
end
|
214
|
+
rescue NoMethodError
|
215
|
+
nil
|
216
|
+
end
|
217
|
+
|
218
|
+
def delete_message
|
219
|
+
qurd_logger.info 'Deleting'
|
220
|
+
begin
|
221
|
+
aws_client(:SQS).delete_message(queue_url: queue_url,
|
222
|
+
receipt_handle: receipt_handle)
|
223
|
+
rescue Aws::SQS::Errors::ReceiptHandleIsInvalid
|
224
|
+
qurd_logger.info('SQS message deleted already or timed out')
|
225
|
+
rescue Aws::SQS::Errors::ServiceError => e
|
226
|
+
qurd_logger.error("SQS raised #{e}")
|
227
|
+
raise e
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|