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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +367 -0
  7. data/Rakefile +9 -0
  8. data/bin/qurd +10 -0
  9. data/lib/hash.rb +6 -0
  10. data/lib/qurd.rb +49 -0
  11. data/lib/qurd/action.rb +92 -0
  12. data/lib/qurd/action/chef.rb +128 -0
  13. data/lib/qurd/action/dummy.rb +27 -0
  14. data/lib/qurd/action/route53.rb +168 -0
  15. data/lib/qurd/configuration.rb +182 -0
  16. data/lib/qurd/listener.rb +207 -0
  17. data/lib/qurd/message.rb +231 -0
  18. data/lib/qurd/mixins.rb +8 -0
  19. data/lib/qurd/mixins/aws_clients.rb +37 -0
  20. data/lib/qurd/mixins/configuration.rb +29 -0
  21. data/lib/qurd/mixins/configuration_helpers.rb +79 -0
  22. data/lib/qurd/processor.rb +97 -0
  23. data/lib/qurd/version.rb +5 -0
  24. data/lib/string.rb +12 -0
  25. data/qurd.gemspec +32 -0
  26. data/test/action_test.rb +115 -0
  27. data/test/chef_test.rb +206 -0
  28. data/test/configuration_test.rb +333 -0
  29. data/test/dummy_action_test.rb +51 -0
  30. data/test/inputs/foo.pem +27 -0
  31. data/test/inputs/knife.rb +9 -0
  32. data/test/inputs/qurd.yml +32 -0
  33. data/test/inputs/qurd_chef.yml +35 -0
  34. data/test/inputs/qurd_chef_route53.yml +43 -0
  35. data/test/inputs/qurd_route53.yml +39 -0
  36. data/test/inputs/qurd_route53_wrong.yml +37 -0
  37. data/test/inputs/validator.pem +27 -0
  38. data/test/listener_test.rb +135 -0
  39. data/test/message_test.rb +187 -0
  40. data/test/mixin_aws_clients_test.rb +28 -0
  41. data/test/mixin_configuration_test.rb +36 -0
  42. data/test/processor_test.rb +41 -0
  43. data/test/responses/aws/ec2-describe-instances-0.xml +2 -0
  44. data/test/responses/aws/ec2-describe-instances-1.xml +127 -0
  45. data/test/responses/aws/error-response.xml +1 -0
  46. data/test/responses/aws/route53-change-resource-record-sets.xml +2 -0
  47. data/test/responses/aws/route53-list-hosted-zones-by-name-0.xml +3 -0
  48. data/test/responses/aws/route53-list-hosted-zones-by-name-1.xml +4 -0
  49. data/test/responses/aws/route53-list-hosted-zones-by-name-n.xml +5 -0
  50. data/test/responses/aws/route53-list-resource-record-sets-0.xml +2 -0
  51. data/test/responses/aws/route53-list-resource-record-sets-1.xml +4 -0
  52. data/test/responses/aws/route53-list-resource-record-sets-n.xml +6 -0
  53. data/test/responses/aws/sqs-list-queues-0.xml +1 -0
  54. data/test/responses/aws/sqs-list-queues-n.xml +4 -0
  55. data/test/responses/aws/sqs-receive-message-1-launch.xml +6 -0
  56. data/test/responses/aws/sqs-receive-message-1-launch_error.xml +6 -0
  57. data/test/responses/aws/sqs-receive-message-1-other.xml +12 -0
  58. data/test/responses/aws/sqs-receive-message-1-terminate.xml +6 -0
  59. data/test/responses/aws/sqs-receive-message-1-terminate_error.xml +6 -0
  60. data/test/responses/aws/sqs-receive-message-1-test.xml +12 -0
  61. data/test/responses/aws/sqs-set-queue-attributes.xml +1 -0
  62. data/test/responses/aws/sts-assume-role.xml +17 -0
  63. data/test/responses/chef/search-client-name-0.json +6 -0
  64. data/test/responses/chef/search-client-name-1.json +7 -0
  65. data/test/responses/chef/search-client-name-n.json +8 -0
  66. data/test/responses/chef/search-node-instance-0.json +5 -0
  67. data/test/responses/chef/search-node-instance-1.json +784 -0
  68. data/test/responses/chef/search-node-instance-n.json +1565 -0
  69. data/test/responses/ec2/latest-meta-data-iam-security-credentials-client.txt +9 -0
  70. data/test/responses/ec2/latest-meta-data-iam-security-credentials.txt +1 -0
  71. data/test/route53_test.rb +231 -0
  72. data/test/support/web_mock_stubs.rb +109 -0
  73. data/test/test_helper.rb +10 -0
  74. 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
@@ -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