elasticity 1.5 → 2.0

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 (57) hide show
  1. data/.rspec +2 -1
  2. data/.rvmrc +1 -1
  3. data/HISTORY.md +47 -24
  4. data/LICENSE +1 -1
  5. data/README.md +165 -317
  6. data/Rakefile +4 -3
  7. data/elasticity.gemspec +3 -5
  8. data/lib/elasticity.rb +10 -5
  9. data/lib/elasticity/aws_request.rb +81 -20
  10. data/lib/elasticity/custom_jar_step.rb +33 -0
  11. data/lib/elasticity/emr.rb +45 -117
  12. data/lib/elasticity/hadoop_bootstrap_action.rb +27 -0
  13. data/lib/elasticity/hive_step.rb +57 -0
  14. data/lib/elasticity/job_flow.rb +109 -39
  15. data/lib/elasticity/job_flow_status.rb +53 -0
  16. data/lib/elasticity/job_flow_status_step.rb +35 -0
  17. data/lib/elasticity/job_flow_step.rb +17 -25
  18. data/lib/elasticity/pig_step.rb +82 -0
  19. data/lib/elasticity/support/conditional_raise.rb +23 -0
  20. data/lib/elasticity/version.rb +1 -1
  21. data/spec/lib/elasticity/aws_request_spec.rb +159 -51
  22. data/spec/lib/elasticity/custom_jar_step_spec.rb +59 -0
  23. data/spec/lib/elasticity/emr_spec.rb +231 -762
  24. data/spec/lib/elasticity/hadoop_bootstrap_action_spec.rb +26 -0
  25. data/spec/lib/elasticity/hive_step_spec.rb +74 -0
  26. data/spec/lib/elasticity/job_flow_integration_spec.rb +197 -0
  27. data/spec/lib/elasticity/job_flow_spec.rb +369 -138
  28. data/spec/lib/elasticity/job_flow_status_spec.rb +147 -0
  29. data/spec/lib/elasticity/job_flow_status_step_spec.rb +73 -0
  30. data/spec/lib/elasticity/job_flow_step_spec.rb +26 -64
  31. data/spec/lib/elasticity/pig_step_spec.rb +104 -0
  32. data/spec/lib/elasticity/support/conditional_raise_spec.rb +35 -0
  33. data/spec/spec_helper.rb +1 -50
  34. data/spec/support/be_a_hash_including_matcher.rb +35 -0
  35. metadata +101 -119
  36. data/.autotest +0 -2
  37. data/lib/elasticity/custom_jar_job.rb +0 -38
  38. data/lib/elasticity/hive_job.rb +0 -69
  39. data/lib/elasticity/pig_job.rb +0 -109
  40. data/lib/elasticity/simple_job.rb +0 -51
  41. data/spec/fixtures/vcr_cassettes/add_instance_groups/one_group_successful.yml +0 -44
  42. data/spec/fixtures/vcr_cassettes/add_instance_groups/one_group_unsuccessful.yml +0 -41
  43. data/spec/fixtures/vcr_cassettes/add_jobflow_steps/add_multiple_steps.yml +0 -266
  44. data/spec/fixtures/vcr_cassettes/custom_jar_job/cloudburst.yml +0 -41
  45. data/spec/fixtures/vcr_cassettes/describe_jobflows/all_jobflows.yml +0 -75
  46. data/spec/fixtures/vcr_cassettes/direct/terminate_jobflow.yml +0 -38
  47. data/spec/fixtures/vcr_cassettes/hive_job/hive_ads.yml +0 -41
  48. data/spec/fixtures/vcr_cassettes/modify_instance_groups/set_instances_to_3.yml +0 -38
  49. data/spec/fixtures/vcr_cassettes/pig_job/apache_log_reports.yml +0 -41
  50. data/spec/fixtures/vcr_cassettes/pig_job/apache_log_reports_with_bootstrap.yml +0 -41
  51. data/spec/fixtures/vcr_cassettes/run_jobflow/word_count.yml +0 -41
  52. data/spec/fixtures/vcr_cassettes/set_termination_protection/nonexistent_job_flows.yml +0 -41
  53. data/spec/fixtures/vcr_cassettes/set_termination_protection/protect_multiple_job_flows.yml +0 -38
  54. data/spec/fixtures/vcr_cassettes/terminate_jobflows/one_jobflow.yml +0 -38
  55. data/spec/lib/elasticity/custom_jar_job_spec.rb +0 -118
  56. data/spec/lib/elasticity/hive_job_spec.rb +0 -90
  57. data/spec/lib/elasticity/pig_job_spec.rb +0 -226
data/Rakefile CHANGED
@@ -4,8 +4,9 @@ Bundler::GemHelper.install_tasks
4
4
  require 'rake/testtask'
5
5
  require 'rspec/core/rake_task'
6
6
 
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ t.verbose = false
9
+ end
10
+
7
11
  desc 'Run specs'
8
12
  task :default => :spec
9
-
10
- desc "Run specs"
11
- RSpec::Core::RakeTask.new
@@ -8,16 +8,14 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Robert Slifka"]
10
10
  s.homepage = "http://www.github.com/rslifka/elasticity"
11
- s.summary = %q{Programmatic access to Amazon's Elastic Map Reduce service.}
12
- s.description = %q{Programmatic access to Amazon's Elastic Map Reduce service, driven by the Sharethrough team's requirements for belting out EMR jobs.}
11
+ s.summary = %q{Streamlined, programmatic access to Amazon's Elastic Map Reduce service.}
12
+ s.description = %q{Streamlined, Programmatic access to Amazon's Elastic Map Reduce service, driven by the Sharethrough team's requirements for belting out EMR jobs.}
13
13
 
14
14
  s.add_dependency("rest-client")
15
15
  s.add_dependency("nokogiri")
16
16
 
17
17
  s.add_development_dependency("rake")
18
- s.add_development_dependency("rspec", ">= 2.8.0")
19
- s.add_development_dependency("vcr", "~> 2.0")
20
- s.add_development_dependency("webmock", "~> 1.8.0")
18
+ s.add_development_dependency("rspec", "~> 2.10.0")
21
19
 
22
20
  s.files = `git ls-files`.split("\n")
23
21
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -4,16 +4,21 @@ require 'time'
4
4
  require 'rest_client'
5
5
  require 'nokogiri'
6
6
 
7
+ require 'elasticity/support/conditional_raise'
8
+
7
9
  require 'elasticity/aws_request'
8
10
  require 'elasticity/emr'
9
- require 'elasticity/job_flow'
11
+
12
+ require 'elasticity/hadoop_bootstrap_action'
10
13
  require 'elasticity/job_flow_step'
11
14
 
12
- require 'elasticity/simple_job'
15
+ require 'elasticity/job_flow'
16
+ require 'elasticity/job_flow_status'
17
+ require 'elasticity/job_flow_status_step'
13
18
 
14
- require 'elasticity/custom_jar_job'
15
- require 'elasticity/hive_job'
16
- require 'elasticity/pig_job'
19
+ require 'elasticity/custom_jar_step'
20
+ require 'elasticity/hive_step'
21
+ require 'elasticity/pig_step'
17
22
 
18
23
  module Elasticity
19
24
  end
@@ -2,52 +2,113 @@ module Elasticity
2
2
 
3
3
  class AwsRequest
4
4
 
5
+ attr_reader :access_key
6
+ attr_reader :secret_key
7
+ attr_reader :options
8
+ attr_reader :host
9
+ attr_reader :protocol
10
+
5
11
  # Supported values for options:
6
12
  # :region - AWS region (e.g. us-west-1)
7
13
  # :secure - true or false, default true.
8
- def initialize(aws_access_key_id, aws_secret_access_key, options = {})
9
- @access_key = aws_access_key_id
10
- @secret_key = aws_secret_access_key
11
- @options = {:secure => true}.merge(options)
14
+ def initialize(access, secret, options = {})
15
+ @access_key = access
16
+ @secret_key = secret
17
+ @host = options[:region] ? "elasticmapreduce.#{options[:region]}.amazonaws.com" : 'elasticmapreduce.amazonaws.com'
18
+ @protocol = {:secure => true}.merge(options)[:secure] ? 'https' : 'http'
12
19
  end
13
20
 
14
- def aws_emr_request(params)
15
- host = @options[:region] ? "elasticmapreduce.#{@options[:region]}.amazonaws.com" : "elasticmapreduce.amazonaws.com"
16
- protocol = @options[:secure] ? "https" : "http"
21
+ def submit(ruby_params)
22
+ aws_params = AwsRequest.convert_ruby_to_aws(ruby_params)
23
+ signed_params = sign_params(aws_params)
24
+ begin
25
+ RestClient.post("#@protocol://#@host", signed_params, :content_type => 'application/x-www-form-urlencoded; charset=utf-8')
26
+ rescue RestClient::BadRequest => e
27
+ raise ArgumentError, AwsRequest.parse_error_response(e.http_body)
28
+ end
29
+ end
17
30
 
18
- signed_params = sign_params(params, "GET", host, "/")
19
- signed_request = "#{protocol}://#{host}?#{signed_params}"
20
- RestClient.get signed_request
31
+ def ==(other)
32
+ return false unless other.is_a? AwsRequest
33
+ return false unless @access_key == other.access_key
34
+ return false unless @secret_key == other.secret_key
35
+ return false unless @options == other.options
36
+ true
21
37
  end
22
38
 
39
+ private
40
+
23
41
  # (Used from RightScale's right_aws gem.)
24
42
  # EC2, SQS, SDB and EMR requests must be signed by this guy.
25
43
  # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
26
44
  # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
27
- def sign_params(service_hash, http_verb, host, uri)
45
+ def sign_params(service_hash)
46
+ uri = '/' # TODO: Why are we hard-coding this?
28
47
  service_hash["AWSAccessKeyId"] = @access_key
29
48
  service_hash["Timestamp"] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
30
49
  service_hash["SignatureVersion"] = "2"
31
- service_hash['SignatureMethod'] = 'HmacSHA256'
50
+ service_hash["SignatureMethod"] = "HmacSHA256"
32
51
  canonical_string = service_hash.keys.sort.map do |key|
33
52
  "#{AwsRequest.aws_escape(key)}=#{AwsRequest.aws_escape(service_hash[key])}"
34
53
  end.join('&')
35
- string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
54
+ string_to_sign = "POST\n#{@host.downcase}\n#{uri}\n#{canonical_string}"
36
55
  signature = AwsRequest.aws_escape(Base64.encode64(OpenSSL::HMAC.digest("sha256", @secret_key, string_to_sign)).strip)
37
56
  "#{canonical_string}&Signature=#{signature}"
38
57
  end
39
58
 
40
- class << self
59
+ # (Used from RightScale's right_aws gem)
60
+ # Escape a string according to Amazon's rules.
61
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
62
+ def self.aws_escape(param)
63
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
64
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
65
+ end
66
+ end
41
67
 
42
- # (Used from RightScale's right_aws gem)
43
- # Escape a string according to Amazon's rules.
44
- # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
45
- def aws_escape(param)
46
- param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
47
- '%' + $1.unpack('H2' * $1.size).join('%').upcase
68
+ # Since we use the same structure as AWS, we can generate AWS param names
69
+ # from the Ruby versions of those names (and the param nesting).
70
+ def self.convert_ruby_to_aws(params)
71
+ result = {}
72
+ params.each do |key, value|
73
+ case value
74
+ when Array
75
+ prefix = "#{camelize(key.to_s)}.member"
76
+ value.each_with_index do |item, index|
77
+ if item.is_a?(String)
78
+ result["#{prefix}.#{index+1}"] = item
79
+ else
80
+ convert_ruby_to_aws(item).each do |nested_key, nested_value|
81
+ result["#{prefix}.#{index+1}.#{nested_key}"] = nested_value
82
+ end
83
+ end
84
+ end
85
+ when Hash
86
+ prefix = "#{camelize(key.to_s)}"
87
+ convert_ruby_to_aws(value).each do |nested_key, nested_value|
88
+ result["#{prefix}.#{nested_key}"] = nested_value
89
+ end
90
+ else
91
+ result[camelize(key.to_s)] = value
48
92
  end
49
93
  end
94
+ result
95
+ end
96
+
97
+ # (Used from Rails' ActiveSupport)
98
+ def self.camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
99
+ if first_letter_in_uppercase
100
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
101
+ else
102
+ lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
103
+ end
104
+ end
50
105
 
106
+ # AWS error responses all follow the same form. Extract the message from
107
+ # the error document.
108
+ def self.parse_error_response(error_xml)
109
+ xml_doc = Nokogiri::XML(error_xml)
110
+ xml_doc.remove_namespaces!
111
+ xml_doc.xpath("/ErrorResponse/Error/Message").text
51
112
  end
52
113
 
53
114
  end
@@ -0,0 +1,33 @@
1
+ module Elasticity
2
+
3
+ class CustomJarStep
4
+
5
+ include JobFlowStep
6
+
7
+ attr_accessor :name
8
+ attr_accessor :jar
9
+ attr_accessor :arguments
10
+ attr_accessor :action_on_failure
11
+
12
+ def initialize(jar)
13
+ @name = "Elasticity Custom Jar Step (#{jar})"
14
+ @jar = jar
15
+ @arguments = []
16
+ @action_on_failure = 'TERMINATE_JOB_FLOW'
17
+ end
18
+
19
+ def to_aws_step(job_flow)
20
+ step = {
21
+ :action_on_failure => @action_on_failure,
22
+ :hadoop_jar_step => {
23
+ :jar => @jar
24
+ },
25
+ :name => @name
26
+ }
27
+ step[:hadoop_jar_step][:args] = @arguments unless @arguments.empty?
28
+ step
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -2,6 +2,8 @@ module Elasticity
2
2
 
3
3
  class EMR
4
4
 
5
+ attr_reader :aws_request
6
+
5
7
  def initialize(aws_access_key_id, aws_secret_access_key, options = {})
6
8
  @aws_request = Elasticity::AwsRequest.new(aws_access_key_id, aws_secret_access_key, options)
7
9
  end
@@ -12,18 +14,14 @@ module Elasticity
12
14
  #
13
15
  # Raises ArgumentError if the specified jobflow does not exist.
14
16
  def describe_jobflow(jobflow_id)
15
- begin
16
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws({
17
- :operation => "DescribeJobFlows",
18
- :job_flow_ids => [jobflow_id]
19
- }))
20
- xml_doc = Nokogiri::XML(aws_result)
21
- xml_doc.remove_namespaces!
22
- yield aws_result if block_given?
23
- JobFlow.from_members_nodeset(xml_doc.xpath("/DescribeJobFlowsResponse/DescribeJobFlowsResult/JobFlows/member")).first
24
- rescue RestClient::BadRequest => e
25
- raise ArgumentError, EMR.parse_error_response(e.http_body)
26
- end
17
+ aws_result = @aws_request.submit({
18
+ :operation => 'DescribeJobFlows',
19
+ :job_flow_ids => [jobflow_id]
20
+ })
21
+ xml_doc = Nokogiri::XML(aws_result)
22
+ xml_doc.remove_namespaces!
23
+ yield aws_result if block_given?
24
+ JobFlowStatus.from_members_nodeset(xml_doc.xpath('/DescribeJobFlowsResponse/DescribeJobFlowsResult/JobFlows/member')).first
27
25
  end
28
26
 
29
27
  # Lists all jobflows in all states.
@@ -33,13 +31,13 @@ module Elasticity
33
31
  #
34
32
  # describe_jobflows(:CreatedBefore => "2011-10-04")
35
33
  def describe_jobflows(params = {})
36
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(
37
- params.merge({:operation => "DescribeJobFlows"}))
34
+ aws_result = @aws_request.submit(
35
+ params.merge({:operation => 'DescribeJobFlows'})
38
36
  )
39
37
  xml_doc = Nokogiri::XML(aws_result)
40
38
  xml_doc.remove_namespaces!
41
39
  yield aws_result if block_given?
42
- JobFlow.from_members_nodeset(xml_doc.xpath("/DescribeJobFlowsResponse/DescribeJobFlowsResult/JobFlows/member"))
40
+ JobFlowStatus.from_members_nodeset(xml_doc.xpath('/DescribeJobFlowsResponse/DescribeJobFlowsResult/JobFlows/member'))
43
41
  end
44
42
 
45
43
  # Adds a new group of instances to the specified jobflow. Elasticity maps a
@@ -62,23 +60,19 @@ module Elasticity
62
60
  # ["ig-2GOVEN6HVJZID", "ig-1DU9M2UQMM051", "ig-3DZRW4Y2X4S", ...]
63
61
  def add_instance_groups(jobflow_id, instance_group_configs)
64
62
  params = {
65
- :operation => "AddInstanceGroups",
63
+ :operation => 'AddInstanceGroups',
66
64
  :job_flow_id => jobflow_id,
67
65
  :instance_groups => instance_group_configs
68
66
  }
69
- begin
70
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
71
- xml_doc = Nokogiri::XML(aws_result)
72
- xml_doc.remove_namespaces!
73
- instance_group_ids = []
74
- xml_doc.xpath("/AddInstanceGroupsResponse/AddInstanceGroupsResult/InstanceGroupIds/member").each do |member|
75
- instance_group_ids << member.text
76
- end
77
- yield aws_result if block_given?
78
- instance_group_ids
79
- rescue RestClient::BadRequest => e
80
- raise ArgumentError, EMR.parse_error_response(e.http_body)
67
+ aws_result = @aws_request.submit(params)
68
+ xml_doc = Nokogiri::XML(aws_result)
69
+ xml_doc.remove_namespaces!
70
+ instance_group_ids = []
71
+ xml_doc.xpath('/AddInstanceGroupsResponse/AddInstanceGroupsResult/InstanceGroupIds/member').each do |member|
72
+ instance_group_ids << member.text
81
73
  end
74
+ yield aws_result if block_given?
75
+ instance_group_ids
82
76
  end
83
77
 
84
78
  # Add a step (or steps) to the specified job flow.
@@ -102,15 +96,11 @@ module Elasticity
102
96
  # })
103
97
  def add_jobflow_steps(jobflow_id, steps_config)
104
98
  params = {
105
- :operation => "AddJobFlowSteps",
99
+ :operation => 'AddJobFlowSteps',
106
100
  :job_flow_id => jobflow_id
107
101
  }.merge!(steps_config)
108
- begin
109
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
110
- yield aws_result if block_given?
111
- rescue RestClient::BadRequest => e
112
- raise ArgumentError, EMR.parse_error_response(e.http_body)
113
- end
102
+ aws_result = @aws_request.submit(params)
103
+ yield aws_result if block_given?
114
104
  end
115
105
 
116
106
  # Set the number of instances in the specified instance groups to the
@@ -123,15 +113,11 @@ module Elasticity
123
113
  # {"ig-1" => 40, "ig-2" => 5, ...}
124
114
  def modify_instance_groups(instance_group_config)
125
115
  params = {
126
- :operation => "ModifyInstanceGroups",
116
+ :operation => 'ModifyInstanceGroups',
127
117
  :instance_groups => instance_group_config.map { |k, v| {:instance_group_id => k, :instance_count => v} }
128
118
  }
129
- begin
130
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
131
- yield aws_result if block_given?
132
- rescue RestClient::BadRequest => e
133
- raise ArgumentError, EMR.parse_error_response(e.http_body)
134
- end
119
+ aws_result = @aws_request.submit(params)
120
+ yield aws_result if block_given?
135
121
  end
136
122
 
137
123
  # Start a job flow with the specified configuration. This is a very thin
@@ -191,17 +177,13 @@ module Elasticity
191
177
  # })
192
178
  def run_job_flow(job_flow_config)
193
179
  params = {
194
- :operation => "RunJobFlow",
180
+ :operation => 'RunJobFlow',
195
181
  }.merge!(job_flow_config)
196
- begin
197
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
198
- yield aws_result if block_given?
199
- xml_doc = Nokogiri::XML(aws_result)
200
- xml_doc.remove_namespaces!
201
- xml_doc.xpath("/RunJobFlowResponse/RunJobFlowResult/JobFlowId").text
202
- rescue RestClient::BadRequest => e
203
- raise ArgumentError, EMR.parse_error_response(e.http_body)
204
- end
182
+ aws_result = @aws_request.submit(params)
183
+ yield aws_result if block_given?
184
+ xml_doc = Nokogiri::XML(aws_result)
185
+ xml_doc.remove_namespaces!
186
+ xml_doc.xpath('/RunJobFlowResponse/RunJobFlowResult/JobFlowId').text
205
187
  end
206
188
 
207
189
  # Enabled or disable "termination protection" on the specified job flows.
@@ -214,16 +196,12 @@ module Elasticity
214
196
  # ["j-1B4D1XP0C0A35", "j-1YG2MYL0HVYS5", ...]
215
197
  def set_termination_protection(jobflow_ids, protection_enabled=true)
216
198
  params = {
217
- :operation => "SetTerminationProtection",
199
+ :operation => 'SetTerminationProtection',
218
200
  :termination_protected => protection_enabled,
219
201
  :job_flow_ids => jobflow_ids
220
202
  }
221
- begin
222
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
223
- yield aws_result if block_given?
224
- rescue RestClient::BadRequest => e
225
- raise ArgumentError, EMR.parse_error_response(e.http_body)
226
- end
203
+ aws_result = @aws_request.submit(params)
204
+ yield aws_result if block_given?
227
205
  end
228
206
 
229
207
  # Terminate the specified jobflow. Amazon does not define a return value
@@ -232,74 +210,24 @@ module Elasticity
232
210
  # flow does not exist.
233
211
  def terminate_jobflows(jobflow_id)
234
212
  params = {
235
- :operation => "TerminateJobFlows",
213
+ :operation => 'TerminateJobFlows',
236
214
  :job_flow_ids => [jobflow_id]
237
215
  }
238
- begin
239
- aws_result = @aws_request.aws_emr_request(EMR.convert_ruby_to_aws(params))
240
- yield aws_result if block_given?
241
- rescue RestClient::BadRequest
242
- raise ArgumentError, "Job flow '#{jobflow_id}' does not exist."
243
- end
216
+ aws_result = @aws_request.submit(params)
217
+ yield aws_result if block_given?
244
218
  end
245
219
 
246
220
  # Pass the specified params hash directly through to the AWS request URL.
247
221
  # Use this if you want to perform an operation that hasn't yet been wrapped
248
222
  # by Elasticity or you just want to see the response XML for yourself :)
249
223
  def direct(params)
250
- @aws_request.aws_emr_request(params)
224
+ @aws_request.submit(params)
251
225
  end
252
226
 
253
- private
254
-
255
- class << self
256
-
257
- # AWS error responses all follow the same form. Extract the message from
258
- # the error document.
259
- def parse_error_response(error_xml)
260
- xml_doc = Nokogiri::XML(error_xml)
261
- xml_doc.remove_namespaces!
262
- xml_doc.xpath("/ErrorResponse/Error/Message").text
263
- end
264
-
265
- # Since we use the same structure as AWS, we can generate AWS param names
266
- # from the Ruby versions of those names (and the param nesting).
267
- def convert_ruby_to_aws(params)
268
- result = {}
269
- params.each do |key, value|
270
- case value
271
- when Array
272
- prefix = "#{camelize(key.to_s)}.member"
273
- value.each_with_index do |item, index|
274
- if item.is_a?(String)
275
- result["#{prefix}.#{index+1}"] = item
276
- else
277
- convert_ruby_to_aws(item).each do |nested_key, nested_value|
278
- result["#{prefix}.#{index+1}.#{nested_key}"] = nested_value
279
- end
280
- end
281
- end
282
- when Hash
283
- prefix = "#{camelize(key.to_s)}"
284
- convert_ruby_to_aws(value).each do |nested_key, nested_value|
285
- result["#{prefix}.#{nested_key}"] = nested_value
286
- end
287
- else
288
- result[camelize(key.to_s)] = value
289
- end
290
- end
291
- result
292
- end
293
-
294
- # (Used from Rails' ActiveSupport)
295
- def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
296
- if first_letter_in_uppercase
297
- lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
298
- else
299
- lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
300
- end
301
- end
302
-
227
+ def ==(other)
228
+ return false unless other.is_a? EMR
229
+ return false unless @aws_request == other.aws_request
230
+ true
303
231
  end
304
232
 
305
233
  end