qurd 0.0.1
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.
- 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
|