elasticity 1.5 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
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