right_aws 1.1.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.
@@ -0,0 +1,3 @@
1
+ == 1.1.0 / 2007-08-10
2
+ Initial release.
3
+
@@ -0,0 +1,21 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/right_aws.rb
6
+ lib/awsbase/benchmark_fix.rb
7
+ lib/awsbase/right_awsbase.rb
8
+ lib/ec2/right_ec2.rb
9
+ lib/s3/right_s3.rb
10
+ lib/s3/right_s3_interface.rb
11
+ lib/sqs/right_sqs.rb
12
+ lib/sqs/right_sqs_interface.rb
13
+ test/ec2/test_helper.rb
14
+ test/ec2/test_right_ec2.rb
15
+ test/s3/test_helper.rb
16
+ test/s3/test_right_s3.rb
17
+ test/sqs/test_helper.rb
18
+ test/sqs/test_right_sqs.rb
19
+ test/ts_right_aws.rb
20
+ test/test_credentials.rb
21
+
@@ -0,0 +1,96 @@
1
+ RightAws
2
+ by RightScale, Inc.
3
+ www.RightScale.com
4
+
5
+ == DESCRIPTION:
6
+
7
+ RightAws::Ec2 is a Ruby library for the Amazon EC2 (Elastic Compute Cloud)
8
+ service.
9
+
10
+ RightAws::S3 and RightAws::S3Interface are Ruby libraries for the Amazon S3
11
+ (Simple Storage Service) service.
12
+
13
+ RightAws::Sqs and RightAws::SqsInterface is a Ruby library for the Amazon SQS (Simple Queue Service)
14
+ service.
15
+
16
+
17
+ All RightAws interfaces work in one of two ways:
18
+ 1) They use a single persistent HTTP connection per
19
+ process or 2) per Ruby thread. Foir example, it doesn't matter how many RightAws::S3
20
+ objects you create, they all use the same per-program or per-thread
21
+ connection. The purpose of sharing the connection is to keep a single
22
+ persistent HTTP connection open to avoid paying connection
23
+ overhead on every request. However, if you have multiple concurrent
24
+ threads, you may want or need an HTTP connection per thread to enable
25
+ concurrent requests to S3. The way this plays out in practice is:
26
+ 1) If you have a non-multithreaded Ruby program, use the non-multithreaded setting for Gem.
27
+ 2) If you have a multi-threaded Ruby program, use the multithreaded setting to enable
28
+ concurrent requests to S3 (SQS, EC2).
29
+ 3) For running under Mongrel/Rails, use thhe non-multithreaded setting for Gem even though
30
+ Mongrel is multithreaded. This is because only one Rails handler is invoked at
31
+ any time (i.e. it acts like a single-threaded program)
32
+
33
+ By default, Ec2/S3/Sqs interface instances are created in single-threaded mode. Set
34
+ "params[:multi_thread]" to "true" in the initialization arguments to use
35
+ multithreaded mode.
36
+
37
+ == FEATURES/PROBLEMS:
38
+
39
+ - Full programmmatic access to Ec2, S3, and Sqs
40
+ - Robust network-level retry layer (using Rightscale::HttpConnection). This includes
41
+ socket connect and read timeouts and retries.
42
+ - Robust HTTP-level retry layer. Certain (user-adjustable) HTTP errors are
43
+ classified as temporary errors. These errors are automaticallly retried
44
+ over exponentially increasing intervals. The number of retries is
45
+ user-configurable.
46
+ - Support for large S3 list operations. Buckets and key subfolders containing
47
+ many (> 1000) keys are listed in entirety. Operations based on list (like
48
+ bucket clear) work on arbitrary numbers of keys.
49
+ - Support for streaming PUTs to S3 if the data source is a file.
50
+ - Support for streaming GETs from S3.
51
+ - Interfaces for HTML link generation.
52
+
53
+ Known Problems:
54
+
55
+ - Amazon recently (8/07) changed the semantics of the SQS service. A
56
+ new queue may not be created within 60 seconds of the destruction of any
57
+ older queue with the same name. Certain methods of RightAws::Sqs and
58
+ RightAws::SqsInterface will fail with the message:
59
+ "AWS.SimpleQueueService.QueueDeletedRecently: You must wait 60 seconds after deleting a queue before you can create another with the same name."
60
+
61
+ == SYNOPSIS:
62
+
63
+
64
+
65
+
66
+
67
+ == REQUIREMENTS:
68
+
69
+ RightAws requires activesupport and RightScale's right_http_connection gem.
70
+
71
+ == INSTALL:
72
+
73
+ sudo gem install
74
+
75
+ == LICENSE:
76
+
77
+ Copyright (c) 2007 RightScale, Inc.
78
+
79
+ Permission is hereby granted, free of charge, to any person obtaining
80
+ a copy of this software and associated documentation files (the
81
+ 'Software'), to deal in the Software without restriction, including
82
+ without limitation the rights to use, copy, modify, merge, publish,
83
+ distribute, sublicense, and/or sell copies of the Software, and to
84
+ permit persons to whom the Software is furnished to do so, subject to
85
+ the following conditions:
86
+
87
+ The above copyright notice and this permission notice shall be
88
+ included in all copies or substantial portions of the Software.
89
+
90
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
91
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
92
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
93
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
94
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
95
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
96
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/right_aws.rb'
6
+
7
+ Hoe.new('right_aws', RightAws::VERSION::STRING) do |p|
8
+ p.rubyforge_name = 'rightaws'
9
+ p.author = 'RightScale, Inc.'
10
+ p.email = 'support@rightscale.com'
11
+ p.summary = 'Interface classes for the Amazon EC2, SQS, and S3 Web Services'
12
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ p.remote_rdoc_dir = "/right_aws_gem_doc"
16
+ p.extra_deps = [['activesupport', '>= 1.4.1'],
17
+ ['right_http_connection','>= 0.1.4']]
18
+ end
19
+
20
+ task :test do
21
+ puts "Hi There"
22
+ require './test/ts_right_aws'
23
+ end
24
+
25
+ # vim: syntax=Ruby
@@ -0,0 +1,39 @@
1
+ #
2
+ # Copyright (c) 2007 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+ #
24
+
25
+
26
+ # A hack because there's a bug in add! in Benchmark::Tms
27
+ module Benchmark #:nodoc:
28
+ class Tms
29
+ def add!(&blk)
30
+ t = Benchmark::measure(&blk)
31
+ @utime = utime + t.utime
32
+ @stime = stime + t.stime
33
+ @cutime = cutime + t.cutime
34
+ @cstime = cstime + t.cstime
35
+ @real = real + t.real
36
+ self
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,231 @@
1
+ #
2
+ # Copyright (c) 2007 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+
25
+ module RightAws
26
+
27
+ AMAZON_PROBLEMS = [ 'internal service error',
28
+ 'is currently unavailable',
29
+ 'no response from',
30
+ 'Please try again',
31
+ 'InternalError',
32
+ 'ServiceUnavailable', #from SQS docs
33
+ 'Unavailable',
34
+ 'This application is not currently available',
35
+ 'InsufficientInstanceCapacity'
36
+ ]
37
+
38
+ # Exception class to handle any Amazon errors
39
+ # Attributes:
40
+ # message - the text of error
41
+ # errors - a list of errors as array or a string(==message if raised manually as AwsError.new('err_text'))
42
+ # request_id - amazon's request id (if exists)
43
+ # http_code - HTTP response error code (if exists)
44
+ class AwsError < RuntimeError
45
+ attr_reader :errors # Array of errors list(each item is an array - [code,message]) or error string
46
+ attr_reader :request_id # Request id (if exists)
47
+ attr_reader :http_code # Response HTTP error code
48
+ def initialize(errors=nil, http_code=nil, request_id=nil)
49
+ @errors = errors
50
+ @request_id = request_id
51
+ @http_code = http_code
52
+ super(@errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s)
53
+ end
54
+ def include?(pattern)
55
+ if @errors.is_a?(Array)
56
+ @errors.each{ |code, msg| return true if code =~ pattern }
57
+ else
58
+ return true if @errors_str =~ pattern
59
+ end
60
+ false
61
+ end
62
+
63
+ def self.on_aws_exception(aws, options={:raise=>true, :log=>true})
64
+ # Only log & notify if not user error
65
+ if !options[:raise] || system_error?($!)
66
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
67
+ puts error_text if options[:puts]
68
+ # Log the error
69
+ if options[:log]
70
+ request = aws.last_request ? aws.last_request.path : '-none-'
71
+ response = aws.last_response ? "#{aws.last_response.code} -- #{aws.last_response.message} -- #{aws.last_response.body}" : '-none-'
72
+ aws.logger.error error_text
73
+ aws.logger.error "Request was: #{request}"
74
+ aws.logger.error "Response was: #{response}"
75
+ end
76
+ end
77
+ raise if options[:raise] # re-raise an exception
78
+ return nil
79
+ end
80
+
81
+ def self.system_error?(e)
82
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
83
+ end
84
+
85
+ end
86
+
87
+
88
+ class AWSErrorHandler
89
+
90
+ @@reiteration_start_delay = 0.2
91
+ def self.reiteration_start_delay
92
+ @@reiteration_start_delay
93
+ end
94
+ def self.reiteration_start_delay=(reiteration_start_delay)
95
+ @@reiteration_start_delay = reiteration_start_delay
96
+ end
97
+
98
+ @@reiteration_time = 5
99
+ def self.reiteration_time
100
+ @@reiteration_time
101
+ end
102
+ def self.reiteration_time=(reiteration_time)
103
+ @@reiteration_time = reiteration_time
104
+ end
105
+
106
+ def initialize(aws, parser, errors_list=nil, reiteration_time=nil) #:nodoc:
107
+ @aws = aws # Link to RightEc2 | RightSqs | RightS3 instance
108
+ @parser = parser # parser to parse Amazon response
109
+ @started_at = Time.now
110
+ @stop_at = @started_at + (reiteration_time || @@reiteration_time)
111
+ @errors_list = errors_list || []
112
+ @reiteration_delay = @@reiteration_start_delay
113
+ @retries = 0
114
+ end
115
+
116
+ # Returns false if
117
+ def check(request) #:nodoc:
118
+ result = false
119
+ error_found = false
120
+ last_errors_text = ''
121
+ response = @aws.last_response
122
+ # log error
123
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
124
+ @aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
125
+ @aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
126
+ # Check response body: if it is an Amazon XML document or not:
127
+ if response.body && response.body[/<\?xml/] # ... it is a xml document
128
+ @aws.class.bench_xml.add! do
129
+ error_parser = RightErrorResponseParser.new
130
+ REXML::Document.parse_stream(response.body, error_parser)
131
+ @aws.last_errors = error_parser.errors
132
+ @aws.last_request_id = error_parser.requestID
133
+ last_errors_text = @aws.last_errors.flatten.join("\n")
134
+ end
135
+ else # ... it is not a xml document(probably just a html page?)
136
+ @aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
137
+ @aws.last_request_id = '-undefined-'
138
+ last_errors_text = response.message
139
+ end
140
+ # now - check the error
141
+ @errors_list.each do |error_to_find|
142
+ if last_errors_text[/#{error_to_find}/i]
143
+ error_found = true
144
+ @aws.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
145
+ break
146
+ end
147
+ end
148
+ # check the time has gone from the first error come
149
+ if error_found
150
+ if (Time.now < @stop_at)
151
+ @retries += 1
152
+ @aws.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
153
+ sleep @reiteration_delay
154
+
155
+ @reiteration_delay *= 2
156
+ result = @aws.request_info(request, @parser)
157
+ else
158
+ @aws.logger.warn("##### Ooops, time is over... ####")
159
+ end
160
+ end
161
+ result
162
+ end
163
+
164
+ end
165
+
166
+
167
+ #-----------------------------------------------------------------
168
+
169
+ class RightAWSParser #:nodoc:
170
+ attr_accessor :result
171
+ attr_reader :xmlpath
172
+ def initialize
173
+ @xmlpath = ''
174
+ @result = false
175
+ @text = ''
176
+ reset
177
+ end
178
+ def tag_start(name, attributes)
179
+ @text = ''
180
+ tagstart(name, attributes)
181
+ @xmlpath += @xmlpath.empty? ? name : "/#{name}"
182
+ end
183
+ def tag_end(name)
184
+ @xmlpath[/^(.*?)\/?#{name}$/]
185
+ @xmlpath = $1
186
+ tagend(name)
187
+ end
188
+ def text(text)
189
+ @text = text
190
+ tagtext(text)
191
+ end
192
+ # Parser must have a lots of methods
193
+ # (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
194
+ # We dont need most of them in RightAWSParser and method_missing helps us
195
+ # to skip their definition
196
+ def method_missing(method, *params)
197
+ # if the method is one of known - just skip it ...
198
+ return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
199
+ :entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
200
+ :doctype].include?(method)
201
+ # ... else - call super to raise an exception
202
+ super(method, params)
203
+ end
204
+ # the functions to be overriden by children (if nessesery)
205
+ def reset ; end
206
+ def tagstart(name, attributes); end
207
+ def tagend(name) ; end
208
+ def tagtext(text) ; end
209
+ end
210
+
211
+ #-----------------------------------------------------------------
212
+ # PARSERS: Errors
213
+ #-----------------------------------------------------------------
214
+
215
+ class RightErrorResponseParser < RightAWSParser #:nodoc:
216
+ attr_accessor :errors # array of hashes: error/message
217
+ attr_accessor :requestID
218
+ def tagend(name)
219
+ case name
220
+ when 'RequestID' ; @requestID = @text
221
+ when 'Code' ; @code = @text
222
+ when 'Message' ; @message = @text
223
+ when 'Error' ; @errors << [ @code, @message ]
224
+ end
225
+ end
226
+ def reset
227
+ @errors = []
228
+ end
229
+ end
230
+
231
+ end
@@ -0,0 +1,1034 @@
1
+ #
2
+ # Copyright (c) 2007 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ module RightAws
25
+
26
+ class Ec2
27
+
28
+ SIGNATURE_VERSION = "1"
29
+ API_VERSION = "2007-01-19"
30
+ DEFAULT_HOST = "ec2.amazonaws.com"
31
+ DEFAULT_PROTOCOL = 'https'
32
+ DEFAULT_PORT = 443
33
+
34
+ DEFAULT_ADDRESSING_TYPE = 'public'
35
+ DNS_ADDRESSING_SET = ['public','direct']
36
+
37
+ # A list if Amazons problems we can handle by AWSErrorHandler.
38
+ @@amazon_problems = RightAws::AMAZON_PROBLEMS
39
+
40
+ # Current aws_access_key_id
41
+ attr_reader :aws_access_key_id
42
+ # Last HTTP request object
43
+ attr_reader :last_request
44
+ # Last HTTP response object
45
+ attr_reader :last_response
46
+ # Last AWS errors list (used by AWSErrorHandler)
47
+ attr_accessor :last_errors
48
+ # Last AWS request id (used by AWSErrorHandler)
49
+ attr_accessor :last_request_id
50
+ # Logger object
51
+ attr_accessor :logger
52
+ # Initial params hash
53
+ attr_accessor :params
54
+
55
+ @@bench_ec2 = Benchmark::Tms.new()
56
+ @@bench_xml = Benchmark::Tms.new()
57
+
58
+ # Benchmark::Tms instance for EC2 access benchmark.
59
+ def self.bench_ec2; @@bench_ec2; end
60
+
61
+ # Benchmark::Tms instance for XML parsing benchmark.
62
+ def self.bench_xml; @@bench_xml; end
63
+
64
+ # Returns a list of Amazon service responses which are known as problems on Amazon side.
65
+ # We have to re-request again if we've got any of them - probably the problem will disappear. By default returns the same value as AMAZON_PROBLEMS const.
66
+ def self.amazon_problems
67
+ @@amazon_problems
68
+ end
69
+
70
+ # Sets a list of Amazon side problems.
71
+ def self.amazon_problems=(problems_list)
72
+ @@amazon_problems = problems_list
73
+ end
74
+
75
+ def initialize(aws_access_key_id, aws_secret_access_key, params={})
76
+ @params = params
77
+ raise AwsError.new("AWS access keys are required to operate on EC2") \
78
+ if aws_access_key_id.blank? || aws_secret_access_key.blank?
79
+ @aws_access_key_id = aws_access_key_id
80
+ @aws_secret_access_key = aws_secret_access_key
81
+ # params
82
+ @params[:server] ||= DEFAULT_HOST
83
+ @params[:port] ||= DEFAULT_PORT
84
+ @params[:protocol] ||= DEFAULT_PROTOCOL
85
+ @params[:multi_thread] ||= defined?(AWS_DAEMON)
86
+ # set logger
87
+ @logger = @params[:logger]
88
+ @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
89
+ @logger = Logger.new(STDOUT) if !@logger
90
+ @logger.info "New #{self.class.name} using #{@params[:multi_thread] ? 'multi' : 'single'}-threaded mode"
91
+ end
92
+
93
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
94
+ AwsError::on_aws_exception(self, options)
95
+ end
96
+
97
+ # Return the +true+ if this RightEc2NativeQuery instance works in multi_thread state and +false+ otherwise.
98
+ def multi_thread
99
+ @params[:multi_thread]
100
+ end
101
+
102
+ def generate_request(action, param={}) #:nodoc:
103
+ timestamp = ( Time::now ).utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
104
+ request_hash = {"Action" => action,
105
+ "AWSAccessKeyId" => @aws_access_key_id,
106
+ "Version" => API_VERSION,
107
+ "Timestamp" => timestamp,
108
+ "SignatureVersion" => SIGNATURE_VERSION }
109
+ request_hash.update(param)
110
+ request_data = request_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
111
+ request_hash.update('Signature' => Base64.encode64( OpenSSL::HMAC.digest( OpenSSL::Digest::Digest.new( "sha1" ), @aws_secret_access_key, request_data)).strip)
112
+ request_params = request_hash.to_a.collect{|key,val| key + "=" + CGI::escape(val) }.join("&")
113
+ request = Net::HTTP::Get.new("/?#{request_params}")
114
+ # prepare output hash
115
+ { :request => request,
116
+ :server => @params[:server],
117
+ :port => @params[:port],
118
+ :protocol => @params[:protocol] }
119
+ end
120
+
121
+ # Sends request to Amazon and parses the response
122
+ # Raises AwsError if any banana happened
123
+ def request_info(request, parser) #:nodoc:
124
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
125
+ thread[:ec2_connection] ||= Rightscale::HttpConnection.new(:exception => AwsError)
126
+ @last_request = request[:request]
127
+ @last_response = nil
128
+ response=nil
129
+
130
+ @@bench_ec2.add!{ response = thread[:ec2_connection].request(request) }
131
+ # check response for errors...
132
+ @last_response = response
133
+ if response.is_a?(Net::HTTPSuccess)
134
+ @error_handler = nil
135
+ @@bench_xml.add! do
136
+ if parser.kind_of?(RightAWSParser)
137
+ REXML::Document.parse_stream(response.body, parser)
138
+ else
139
+ parser.parse(response)
140
+ end
141
+ end
142
+ return parser.result
143
+ else
144
+ @error_handler = AWSErrorHandler.new(self, parser, @@amazon_problems) unless @error_handler
145
+ check_result = @error_handler.check(request)
146
+ if check_result
147
+ @error_handler = nil
148
+ return check_result
149
+ end
150
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
151
+ end
152
+ rescue
153
+ @error_handler = nil
154
+ raise
155
+ end
156
+
157
+
158
+ def hash_params(prefix, list) #:nodoc:
159
+ groups = {}
160
+ list.each_index{|i| groups.update("#{prefix}.#{i+1}"=>list[i])} if list
161
+ return groups
162
+ end
163
+
164
+
165
+ #-----------------------------------------------------------------
166
+ #-----------------------------------------------------------------
167
+ #-----------------------------------------------------------------
168
+
169
+ def ec2_describe_images(type, list) #:nodoc:
170
+ link = generate_request("DescribeImages", hash_params(type,list))
171
+ return request_info(link, QEc2DescribeImagesParser.new)
172
+ end
173
+
174
+ def ec2_describe_images_by_id( list) #:nodoc:
175
+ return ec2_describe_images('ImageId', list)
176
+ end
177
+
178
+ def ec2_describe_images_by_owner(list) #:nodoc:
179
+ return ec2_describe_images('Owner', list)
180
+ end
181
+
182
+ def ec2_describe_images_by_executable_by(list) #:nodoc:
183
+ return ec2_describe_images('ExecutableBy', list)
184
+ end
185
+
186
+ # Retrieve a list of images. Returns array of hashes or an exception:
187
+ #
188
+ # ec2.describe_images #=>
189
+ # [{:aws_owner=>"522821470517", :aws_id=>"ami-e4b6538d", :aws_state=>"available",
190
+ # :aws_location=>"marcins_cool_public_images/ubuntu-6.10.manifest.xml",:aws_is_public=>true},
191
+ # ..., {...} ]
192
+ #
193
+ # If +list+ param is set, then retrieve information about the listed images only:
194
+ #
195
+ # ec2.describe_images(['ami-e4b6538d']) #=>
196
+ # [{:aws_owner=>"522821470517", :aws_id=>"ami-e4b6538d", :aws_state=>"available",
197
+ # :aws_location=>"marcins_cool_public_images/ubuntu-6.10.manifest.xml",:aws_is_public=>true}]
198
+ #
199
+ def describe_images(list=[])
200
+ images = list.nil? ? ec2_describe_images_by_executable_by(['self']) : ec2_describe_images_by_id(list)
201
+ images.collect! do |image|
202
+ {:aws_id => image.imageId,
203
+ :aws_location => image.imageLocation,
204
+ :aws_owner => image.imageOwnerId,
205
+ :aws_state => image.imageState.downcase,
206
+ :aws_is_public => image.isPublic }
207
+ end
208
+ images
209
+ rescue Exception
210
+ on_exception
211
+ end
212
+
213
+ # Register new image at Amazon.
214
+ # Returns new image id or an exception.
215
+ #
216
+ # ec2.register_image('bucket/key/manifest') #=> 'ami-e444444d'
217
+ #
218
+ def register_image(image_location)
219
+ link = generate_request("RegisterImage",
220
+ 'ImageLocation' => image_location.to_s)
221
+ request_info(link, QEc2RegisterImageParser.new)
222
+ rescue Exception
223
+ on_exception
224
+ end
225
+
226
+ # Deregister image at Amazon. Returns +true+ or an exception.
227
+ #
228
+ # ec2.deregister_image('ami-e444444d') #=> true
229
+ #
230
+ def deregister_image(image_id)
231
+ link = generate_request("DeregisterImage",
232
+ 'ImageId' => image_id.to_s)
233
+ request_info(link, RightBoolResponseParser.new)
234
+ rescue Exception
235
+ on_exception
236
+ end
237
+
238
+
239
+ # Describe image attributes. Currently, only 'launchPermission' is supported.
240
+ #
241
+ # ec2.describe_image_attribute('ami-e444444d') #=> {:groups=>["all"], :users=>["000000000777"]}
242
+ #
243
+ def describe_image_attribute(image_id, attribute='launchPermission')
244
+ link = generate_request("DescribeImageAttribute",
245
+ 'ImageId' => image_id,
246
+ 'Attribute' => attribute)
247
+ image_attr = request_info(link, QEc2DescribeImageAttributeParser.new)
248
+ { :users => image_attr.launchPermission.userIds,
249
+ :groups => image_attr.launchPermission.groups }
250
+ rescue Exception
251
+ on_exception
252
+ end
253
+
254
+ # Reset image attribute. Currently, only 'launchPermission' is supported. Returns +true+ or an exception.
255
+ #
256
+ # ec2.reset_image_attribute('ami-e444444d') #=> true
257
+ #
258
+ def reset_image_attribute(image_id, attribute='launchPermission')
259
+ link = generate_request("ResetImageAttribute",
260
+ 'ImageId' => image_id,
261
+ 'Attribute' => attribute)
262
+ request_info(link, RightBoolResponseParser.new)
263
+ rescue Exception
264
+ on_exception
265
+ end
266
+
267
+ # Users should prefer modify_image_launch_perm_add|remove_users|groups() instead of modify_image_attribute() because the signature of
268
+ # modify_image_attribute() may change with EC2 service changes.
269
+ #
270
+ # attribute : currently, only 'launchPermission' is supported.
271
+ # operationType : currently, only 'add' & 'remove' are supported.
272
+ # userGroup : currently, only 'all' is supported.
273
+ def modify_image_attribute(image_id, attribute, operation_type, user_id=[], user_group=[])
274
+ user_id = user_id.to_a
275
+ user_group = user_group.to_a
276
+ params = {'ImageId' => image_id,
277
+ 'Attribute' => attribute,
278
+ 'OperationType' => operation_type}
279
+ params.update(hash_params('UserId', user_id)) unless user_id.empty?
280
+ params.update(hash_params('UserGroup', user_group)) unless user_group.empty?
281
+ link = generate_request("ModifyImageAttribute", params)
282
+ request_info(link, RightBoolResponseParser.new)
283
+ rescue Exception
284
+ on_exception
285
+ end
286
+
287
+ # Grant image launch permissions for users. Parameter +userId+ is a list of users' AWS account ids. Returns +true+ or an exception.
288
+ #
289
+ # ec2.modify_image_launch_perm_add_users('ami-e444444d',['000000000777','000000000778']) #=> true
290
+ #
291
+ def modify_image_launch_perm_add_users(image_id, user_id=[])
292
+ modify_image_attribute(image_id, 'launchPermission', 'add', user_id, [])
293
+ end
294
+
295
+ # Revokes image launch permissions for users. +userId+ is a list of users AWS accounts ids. Returns +true+ or an exception.
296
+ #
297
+ # ec2.modify_image_launch_perm_remove_users('ami-e444444d',['000000000777','000000000778']) #=> true
298
+ #
299
+ def modify_image_launch_perm_remove_users(image_id, user_id=[])
300
+ modify_image_attribute(image_id, 'launchPermission', 'remove', user_id, [])
301
+ end
302
+
303
+ # Add image launch permissions for users groups (currently only 'all' is supported, which gives public launch permissions).
304
+ # Returns +true+ or an exception.
305
+ #
306
+ # ec2.modify_image_launch_perm_add_groups('ami-e444444d') #=> true
307
+ #
308
+ def modify_image_launch_perm_add_groups(image_id, userGroup=['all'])
309
+ modify_image_attribute(image_id, 'launchPermission', 'add', [], userGroup)
310
+ end
311
+
312
+ # Remove image launch permissions for users groups (currently only 'all' is supported, which gives public launch permissions).
313
+ #
314
+ # ec2.modify_image_launch_perm_remove_groups('ami-e444444d') #=> true
315
+ #
316
+ def modify_image_launch_perm_remove_groups(image_id, userGroup=['all'])
317
+ modify_image_attribute(image_id, 'launchPermission', 'remove', [], userGroup)
318
+ end
319
+
320
+ def get_desc_instances(instances) # :nodoc:
321
+ result = []
322
+ instances.each do |item|
323
+ item.instancesSet.each do |instance|
324
+ # Parse and remove timestamp from the reason string. The timestamp is of
325
+ # the request, not when EC2 took action, thus confusing & useless...
326
+ reason = instance.reason.sub(/\(\d[^)]*GMT\) */, '')
327
+ result << {:aws_owner => item.ownerId,
328
+ :aws_reservation_id => item.reservationId,
329
+ :aws_groups => item.groupSet,
330
+ :aws_state_code => instance.instanceState.code,
331
+ :dns_name => instance.dnsName,
332
+ :private_dns_name => instance.privateDnsName,
333
+ :aws_instance_id => instance.instanceId,
334
+ :aws_state => instance.instanceState.name,
335
+ :ssh_key_name => instance.keyName,
336
+ :aws_image_id => instance.imageId,
337
+ :aws_reason => reason}
338
+ end
339
+ end
340
+ result
341
+ rescue Exception
342
+ on_exception
343
+ end
344
+
345
+ # Retrieve information about EC2 instances. If +list+ is omitted then returns the whole list.
346
+ #
347
+ # ec2.describe_instances #=>
348
+ # [{:aws_image_id => "ami-e444444d",
349
+ # :aws_reason => "",
350
+ # :aws_state_code => "16",
351
+ # :aws_owner => "000000000888",
352
+ # :aws_instance_id => "i-123f1234",
353
+ # :aws_reservation_id => "r-aabbccdd",
354
+ # :aws_state => "running",
355
+ # :dns_name => "domU-12-34-67-89-01-C9.usma2.compute.amazonaws.com",
356
+ # :ssh_key_name => "staging",
357
+ # :aws_groups => ["default"],
358
+ # :private_dns_name => "domU-12-34-67-89-01-C9.usma2.compute.amazonaws.com"},
359
+ # ..., {...}]
360
+ #
361
+ def describe_instances(list=[])
362
+ link = generate_request("DescribeInstances", hash_params('InstanceId',list))
363
+ instances = request_info(link, QEc2DescribeInstancesParser.new)
364
+ get_desc_instances(instances)
365
+ rescue Exception
366
+ on_exception
367
+ end
368
+
369
+ # Launch new EC2 instances. Returns a list of launched instances or an exception.
370
+ #
371
+ # ec2.run_instances('ami-e444444d',1,1,['my_awesome_group'],'my_awesome_key', 'Woohoo!!!', public) #=>
372
+ # [{:aws_image_id => "ami-e444444d",
373
+ # :aws_reason => "",
374
+ # :aws_state_code => "0",
375
+ # :aws_owner => "000000000888",
376
+ # :aws_instance_id => "i-123f1234",
377
+ # :aws_reservation_id => "r-aabbccdd",
378
+ # :aws_state => "pending",
379
+ # :dns_name => "",
380
+ # :ssh_key_name => "my_awesome_key",
381
+ # :aws_groups => ["my_awesome_group"],
382
+ # :private_dns_name => ""}]
383
+ #
384
+ def run_instances(image_id, min_count, max_count, group_ids, key_name, user_data='', addressing_type=DEFAULT_ADDRESSING_TYPE)
385
+ @logger.info("Launching instance of image #{image_id} for #{@aws_access_key_id}, key: #{key_name}, groups: #{(group_ids||[]).join(',')}")
386
+ # careful: keyName and securityGroups may be nil
387
+ params = hash_params('SecurityGroup', group_ids)
388
+ params.update( {'ImageId' => image_id,
389
+ 'MinCount' => min_count.to_s,
390
+ 'MaxCount' => max_count.to_s,
391
+ 'AddressingType' => addressing_type })
392
+ params['KeyName'] = key_name unless key_name.blank?
393
+ unless user_data.blank?
394
+ user_data.strip!
395
+ # Do not use CGI::escape(encode64(...)) as it is done in Amazons EC2 library.
396
+ # Amazon 169.254.169.254 does not like escaped symbols!
397
+ # And it dont like "\n" inside of encoded string! Grrr....
398
+ # Otherwise, some of UserData symbols will be lost...
399
+ params['UserData'] = Base64.encode64(user_data).delete("\n") unless user_data.empty?
400
+ end
401
+ link = generate_request("RunInstances", params)
402
+ #debugger
403
+ instances = request_info(link, QEc2RunInstancesParser.new)
404
+ get_desc_instances(instances)
405
+ rescue Exception
406
+ on_exception
407
+ end
408
+
409
+ # Terminates EC2 instances. Returns a list of termination params or an exception.
410
+ #
411
+ # ec2.terminate_instances(['i-f222222d','i-f222222e']) #=>
412
+ # [{:aws_shutdown_state => "shutting-down",
413
+ # :aws_instance_id => "i-f222222d",
414
+ # :aws_shutdown_state_code => 32,
415
+ # :aws_prev_state => "running",
416
+ # :aws_prev_state_code => 16},
417
+ # {:aws_shutdown_state => "shutting-down",
418
+ # :aws_instance_id => "i-f222222e",
419
+ # :aws_shutdown_state_code => 32,
420
+ # :aws_prev_state => "running",
421
+ # :aws_prev_state_code => 16}]
422
+ #
423
+ def terminate_instances(list=[])
424
+ link = generate_request("TerminateInstances", hash_params('InstanceId',list))
425
+ instances = request_info(link, QEc2TerminateInstancesParser.new)
426
+ instances.collect! do |instance|
427
+ { :aws_instance_id => instance.instanceId,
428
+ :aws_shutdown_state => instance.shutdownState.name,
429
+ :aws_shutdown_state_code => instance.shutdownState.code.to_i,
430
+ :aws_prev_state => instance.previousState.name,
431
+ :aws_prev_state_code => instance.previousState.code.to_i }
432
+ end
433
+ instances
434
+ rescue Exception
435
+ on_exception
436
+ end
437
+
438
+ # Retreive EC2 instance OS logs. Returns a hash of data or an exception.
439
+ #
440
+ # ec2.get_console_output('i-f222222d') =>
441
+ # {:aws_instance_id => 'i-f222222d',
442
+ # :aws_timestamp => "2007-05-23T14:36:07.000-07:00",
443
+ # :timestamp => Wed May 23 21:36:07 UTC 2007, # Time instance
444
+ # :aws_output => "Linux version 2.6.16-xenU (builder@patchbat.amazonsa) (gcc version 4.0.1 20050727 ..."
445
+ def get_console_output(instance_id)
446
+ link = generate_request("GetConsoleOutput", { 'InstanceId.1' => instance_id })
447
+ result = request_info(link, QEc2GetConsoleOutputParser.new)
448
+ { :aws_instance_id => result.instanceId,
449
+ :aws_timestamp => result.timestamp,
450
+ :timestamp => (Time.parse(result.timestamp)).utc,
451
+ :aws_output => result.output }
452
+ rescue Exception
453
+ on_exception
454
+ end
455
+
456
+ # Reboot an EC2 instance. Returns +true+ or an exception.
457
+ #
458
+ # ec2.reboot_instances(['i-f222222d','i-f222222e']) #=> true
459
+ #
460
+ def reboot_instances(list)
461
+ link = generate_request("RebootInstances", hash_params('InstanceId', list.to_a))
462
+ request_info(link, RightBoolResponseParser.new)
463
+ rescue Exception
464
+ on_exception
465
+ end
466
+
467
+ # Retrieve Security Group information. If +list+ is omitted the returns the whole list of groups.
468
+ #
469
+ # ec2.describe_security_groups #=>
470
+ # [{:aws_group_name => "default-1",
471
+ # :aws_owner => "000000000888",
472
+ # :aws_description => "Default allowing SSH, HTTP, and HTTPS ingress",
473
+ # :aws_perms =>
474
+ # [{:owner => "000000000888", :group => "default"},
475
+ # {:owner => "000000000888", :group => "default-1"},
476
+ # {:to_port => "-1", :protocol => "icmp", :from_port => "-1", :cidr_ips => "0.0.0.0/0"},
477
+ # {:to_port => "22", :protocol => "tcp", :from_port => "22", :cidr_ips => "0.0.0.0/0"},
478
+ # {:to_port => "80", :protocol => "tcp", :from_port => "80", :cidr_ips => "0.0.0.0/0"},
479
+ # {:to_port => "443", :protocol => "tcp", :from_port => "443", :cidr_ips => "0.0.0.0/0"}]},
480
+ # ..., {...}]
481
+ #
482
+ def describe_security_groups(list=[])
483
+ link = generate_request("DescribeSecurityGroups", hash_params('GroupName',list))
484
+ groups = request_info(link, QEc2DescribeSecurityGroupsParser.new)
485
+
486
+ result = []
487
+ groups.each do |item|
488
+ perms = []
489
+ item.ipPermissions.each do |perm|
490
+ perm.groups.each do |ngroup|
491
+ perms << {:group => ngroup.groupName,
492
+ :owner => ngroup.userId}
493
+ end
494
+ perm.ipRanges.each do |cidr_ip|
495
+ perms << {:from_port => perm.fromPort,
496
+ :to_port => perm.toPort,
497
+ :protocol => perm.ipProtocol,
498
+ :cidr_ips => cidr_ip}
499
+ end
500
+ end
501
+
502
+ # delete duplication
503
+ perms.each_index do |i|
504
+ (0...i).each do |j|
505
+ if perms[i] == perms[j] then perms[i] = nil; break; end
506
+ end
507
+ end
508
+ perms.compact!
509
+
510
+ result << {:aws_owner => item.ownerId,
511
+ :aws_group_name => item.groupName,
512
+ :aws_description => item.groupDescription,
513
+ :aws_perms => perms}
514
+ end
515
+ result
516
+ rescue Exception
517
+ on_exception
518
+ end
519
+
520
+ # Create new Security Group. Returns +true+ or an exception.
521
+ #
522
+ # ec2.create_security_group('default-1',"Default allowing SSH, HTTP, and HTTPS ingress") #=> true
523
+ #
524
+ def create_security_group(name, description)
525
+ # EC2 doesn't like an empty description...
526
+ description = " " if description.blank?
527
+ link = generate_request("CreateSecurityGroup",
528
+ 'GroupName' => name.to_s,
529
+ 'GroupDescription' => description.to_s)
530
+ request_info(link, RightBoolResponseParser.new)
531
+ rescue Exception
532
+ on_exception
533
+ end
534
+
535
+ # Remove Security Group. Returns +true+ or an exception.
536
+ #
537
+ # ec2.delete_security_group('default-1') #=> true
538
+ #
539
+ def delete_security_group(name)
540
+ link = generate_request("DeleteSecurityGroup",
541
+ 'GroupName' => name.to_s)
542
+ request_info(link, RightBoolResponseParser.new)
543
+ rescue Exception
544
+ on_exception
545
+ end
546
+
547
+ # Authorize named ingress for security group.
548
+ #
549
+ # ec2.authorize_security_group_named_ingress('my_awesome_group', aws_user_id, 'another_group_name') #=> true
550
+ #
551
+ def authorize_security_group_named_ingress(name, owner, group)
552
+ link = generate_request("AuthorizeSecurityGroupIngress",
553
+ 'GroupName' => name.to_s,
554
+ 'SourceSecurityGroupName' => group.to_s,
555
+ 'SourceSecurityGroupOwnerId' => owner.to_s.gsub(/-/,''))
556
+ request_info(link, RightBoolResponseParser.new)
557
+ rescue Exception
558
+ on_exception
559
+ end
560
+
561
+ # Revoke named ingress for security group.
562
+ #
563
+ # ec2.revoke_security_group_named_ingress('my_awesome_group', aws_user_id, 'another_group_name') #=> true
564
+ #
565
+ def revoke_security_group_named_ingress(name, owner, group)
566
+ link = generate_request("RevokeSecurityGroupIngress",
567
+ 'GroupName' => name.to_s,
568
+ 'SourceSecurityGroupName' => group.to_s,
569
+ 'SourceSecurityGroupOwnerId' => owner.to_s.gsub(/-/,''))
570
+ request_info(link, RightBoolResponseParser.new)
571
+ rescue Exception
572
+ on_exception
573
+ end
574
+
575
+ # Add permission to a security group. Returns +true+ or an exception. +protocol+ is one of :'tcp'|'udp'|'icmp' ('tcp' is default).
576
+ #
577
+ # ec2.authorize_security_group_IP_ingress('my_awesome_group', 80, 82, 'udp', '192.168.1.0/8') #=> true
578
+ # ec2.authorize_security_group_IP_ingress('my_awesome_group', -1, -1, 'icmp') #=> true
579
+ #
580
+ def authorize_security_group_IP_ingress(name, from_port, to_port, protocol='tcp', cidr_ip='0.0.0.0/0')
581
+ link = generate_request("AuthorizeSecurityGroupIngress",
582
+ 'GroupName' => name.to_s,
583
+ 'IpProtocol' => protocol.to_s,
584
+ 'FromPort' => from_port.to_s,
585
+ 'ToPort' => to_port.to_s,
586
+ 'CidrIp' => cidr_ip.to_s)
587
+ request_info(link, RightBoolResponseParser.new)
588
+ rescue Exception
589
+ on_exception
590
+ end
591
+
592
+ # Remove permission from a security group. Returns +true+ or an exception. +protocol+ is one of :'tcp'|'udp'|'icmp' ('tcp' is default).
593
+ #
594
+ # ec2.revoke_security_group_IP_ingress('my_awesome_group', 80, 82, 'udp', '192.168.1.0/8') #=> true
595
+ #
596
+ def revoke_security_group_IP_ingress(name, from_port, to_port, protocol='tcp', cidr_ip='0.0.0.0/0')
597
+ link = generate_request("RevokeSecurityGroupIngress",
598
+ 'GroupName' => name.to_s,
599
+ 'IpProtocol' => protocol.to_s,
600
+ 'FromPort' => from_port.to_s,
601
+ 'ToPort' => to_port.to_s,
602
+ 'CidrIp' => cidr_ip.to_s)
603
+ request_info(link, RightBoolResponseParser.new)
604
+ rescue Exception
605
+ on_exception
606
+ end
607
+
608
+ # Retrieve a list of SSH keys. Returms an array of keys or an exception.
609
+ #
610
+ # ec2.describe_key_pairs #=>
611
+ # [{:aws_fingerprint=> "01:02:03:f4:25:e6:97:e8:9b:02:1a:26:32:4e:58:6b:7a:8c:9f:03", :aws_key_name=>"key-1"},
612
+ # {:aws_fingerprint=> "1e:29:30:47:58:6d:7b:8c:9f:08:11:20:3c:44:52:69:74:80:97:08", :aws_key_name=>"key-2"},
613
+ # ..., {...} ]
614
+ #
615
+ def describe_key_pairs(list=[])
616
+ link = generate_request("DescribeKeyPairs", hash_params('KeyName',list))
617
+ result = request_info(link, QEc2DescribeKeyPairParser.new)
618
+ result.collect! do |key|
619
+ {:aws_key_name => key.keyName,
620
+ :aws_fingerprint => key.keyFingerprint }
621
+ end
622
+ result
623
+ rescue Exception
624
+ on_exception
625
+ end
626
+
627
+ # Create new SSH key. Returns a hash of key's data or an exception.
628
+ #
629
+ # ec2.create_key_pair('my_awesome_key') #=>
630
+ # {:aws_key_name => "my_awesome_key",
631
+ # :aws_fingerprint => "01:02:03:f4:25:e6:97:e8:9b:02:1a:26:32:4e:58:6b:7a:8c:9f:03",
632
+ # :aws_material => "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAK...Q8MDrCbuQ=\n-----END RSA PRIVATE KEY-----"}
633
+ #
634
+ def create_key_pair(name)
635
+ link = generate_request("CreateKeyPair",
636
+ 'KeyName' => name.to_s)
637
+ key = request_info(link, QEc2CreateKeyPairParser.new)
638
+ { :aws_key_name => key.keyName,
639
+ :aws_fingerprint => key.keyFingerprint,
640
+ :aws_material => key.keyMaterial}
641
+ rescue Exception
642
+ on_exception
643
+ end
644
+
645
+ # Delete key pair. Returns +true+ or an exception.
646
+ #
647
+ # ec2.delete_key_pair('my_awesome_key') #=> true
648
+ #
649
+ def delete_key_pair(name)
650
+ link = generate_request("DeleteKeyPair",
651
+ 'KeyName' => name.to_s)
652
+ request_info(link, RightBoolResponseParser.new)
653
+ rescue Exception
654
+ on_exception
655
+ end
656
+
657
+
658
+ #-----------------------------------------------------------------
659
+ # PARSERS: Boolean Response Parser
660
+ #-----------------------------------------------------------------
661
+
662
+ class RightBoolResponseParser < RightAWSParser # :nodoc:
663
+ def tagend(name)
664
+ @result = @text=='true' ? true : false if name == 'return'
665
+ end
666
+ end
667
+
668
+ #-----------------------------------------------------------------
669
+ # PARSERS: Key Pair
670
+ #-----------------------------------------------------------------
671
+
672
+ class QEc2DescribeKeyPairType # :nodoc:
673
+ attr_accessor :keyName
674
+ attr_accessor :keyFingerprint
675
+ end
676
+
677
+ class QEc2CreateKeyPairType < QEc2DescribeKeyPairType # :nodoc:
678
+ attr_accessor :keyMaterial
679
+ end
680
+
681
+ class QEc2DescribeKeyPairParser < RightAWSParser # :nodoc:
682
+ def tagstart(name, attributes)
683
+ @item = QEc2DescribeKeyPairType.new if name == 'item'
684
+ end
685
+ def tagend(name)
686
+ case name
687
+ when 'keyName' ; @item.keyName = @text
688
+ when 'keyFingerprint'; @item.keyFingerprint = @text
689
+ when 'item' ; @result << @item
690
+ end
691
+ end
692
+ def reset
693
+ @result = [];
694
+ end
695
+ end
696
+
697
+ class QEc2CreateKeyPairParser < RightAWSParser # :nodoc:
698
+ def tagstart(name, attributes)
699
+ @result = QEc2CreateKeyPairType.new if !@result
700
+ end
701
+ def tagend(name)
702
+ case name
703
+ when 'keyName' ; @result.keyName = @text
704
+ when 'keyFingerprint' ; @result.keyFingerprint = @text
705
+ when 'keyMaterial' ; @result.keyMaterial = @text
706
+ end
707
+ end
708
+ end
709
+
710
+ #-----------------------------------------------------------------
711
+ # PARSERS: Security Groups
712
+ #-----------------------------------------------------------------
713
+
714
+ class QEc2UserIdGroupPairType # :nodoc:
715
+ attr_accessor :userId
716
+ attr_accessor :groupName
717
+ end
718
+
719
+ class QEc2IpPermissionType # :nodoc:
720
+ attr_accessor :ipProtocol
721
+ attr_accessor :fromPort
722
+ attr_accessor :toPort
723
+ attr_accessor :groups
724
+ attr_accessor :ipRanges
725
+ end
726
+
727
+ class QEc2SecurityGroupItemType # :nodoc:
728
+ attr_accessor :groupName
729
+ attr_accessor :groupDescription
730
+ attr_accessor :ownerId
731
+ attr_accessor :ipPermissions
732
+ end
733
+
734
+
735
+ class QEc2DescribeSecurityGroupsParser < RightAWSParser # :nodoc:
736
+ def tagstart(name, attributes)
737
+ case name
738
+ when 'item'
739
+ if @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo'
740
+ @group = QEc2SecurityGroupItemType.new
741
+ @group.ipPermissions = []
742
+ elsif @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item/ipPermissions'
743
+ @perm = QEc2IpPermissionType.new
744
+ @perm.ipRanges = []
745
+ @perm.groups = []
746
+ elsif @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item/ipPermissions/item/groups'
747
+ @sgroup = QEc2UserIdGroupPairType.new
748
+ end
749
+ end
750
+ end
751
+ def tagend(name)
752
+ case name
753
+ when 'ownerId' ; @group.ownerId = @text
754
+ when 'groupDescription' ; @group.groupDescription = @text
755
+ when 'groupName'
756
+ if @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item'
757
+ @group.groupName = @text
758
+ elsif @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item/ipPermissions/item/groups/item'
759
+ @sgroup.groupName = @text
760
+ end
761
+ when 'ipProtocol' ; @perm.ipProtocol = @text
762
+ when 'fromPort' ; @perm.fromPort = @text
763
+ when 'toPort' ; @perm.toPort = @text
764
+ when 'userId' ; @sgroup.userId = @text
765
+ when 'cidrIp' ; @perm.ipRanges << @text
766
+ when 'item'
767
+ if @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item/ipPermissions/item/groups'
768
+ @perm.groups << @sgroup
769
+ elsif @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo/item/ipPermissions'
770
+ @group.ipPermissions << @perm
771
+ elsif @xmlpath=='DescribeSecurityGroupsResponse/securityGroupInfo'
772
+ @result << @group
773
+ end
774
+ end
775
+ end
776
+ def reset
777
+ @result = []
778
+ end
779
+ end
780
+
781
+ #-----------------------------------------------------------------
782
+ # PARSERS: Images
783
+ #-----------------------------------------------------------------
784
+
785
+ class QEc2DescribeImagesResponseItemType # :nodoc:
786
+ attr_accessor :imageId
787
+ attr_accessor :imageState
788
+ attr_accessor :imageLocation
789
+ attr_accessor :imageOwnerId
790
+ attr_accessor :isPublic
791
+ end
792
+
793
+ class QEc2DescribeImagesParser < RightAWSParser # :nodoc:
794
+ def tagstart(name, attributes)
795
+ @image = QEc2DescribeImagesResponseItemType.new if name == 'item'
796
+ end
797
+ def tagend(name)
798
+ case name
799
+ when 'imageId' ; @image.imageId = @text
800
+ when 'imageLocation' ; @image.imageLocation = @text
801
+ when 'imageState' ; @image.imageState = @text
802
+ when 'imageOwnerId' ; @image.imageOwnerId = @text
803
+ when 'isPublic' ; @image.isPublic = @text == 'true' ? true : false
804
+ when 'item' ; @result << @image
805
+ end
806
+ end
807
+ def reset
808
+ @result = []
809
+ end
810
+ end
811
+
812
+ class QEc2RegisterImageParser < RightAWSParser # :nodoc:
813
+ def tagend(name)
814
+ @result = @text if name == 'imageId'
815
+ end
816
+ end
817
+
818
+ #-----------------------------------------------------------------
819
+ # PARSERS: Image Attribute
820
+ #-----------------------------------------------------------------
821
+
822
+ class QEc2LaunchPermissionItemType # :nodoc:
823
+ attr_accessor :groups
824
+ attr_accessor :userIds
825
+ end
826
+
827
+ class QEc2DescribeImageAttributeType # :nodoc:
828
+ attr_accessor :imageId
829
+ attr_accessor :launchPermission
830
+ end
831
+
832
+ class QEc2DescribeImageAttributeParser < RightAWSParser # :nodoc:
833
+ def tagstart(name, attributes)
834
+ case name
835
+ when 'launchPermission'
836
+ @result.launchPermission = QEc2LaunchPermissionItemType.new
837
+ @result.launchPermission.groups = []
838
+ @result.launchPermission.userIds = []
839
+ end
840
+ end
841
+ def tagend(name)
842
+ # right now only 'launchPermission' is supported by Amazon.
843
+ # But nobody know what will they xml later as attribute. That is why we
844
+ # check for 'group' and 'userId' inside of 'launchPermission/item'
845
+ case name
846
+ when 'imageId' ; @result.imageId = @text
847
+ when 'group'
848
+ @result.launchPermission.groups << @text if @xmlpath == 'DescribeImageAttributeResponse/launchPermission/item'
849
+ when 'userId'
850
+ @result.launchPermission.userIds << @text if @xmlpath == 'DescribeImageAttributeResponse/launchPermission/item'
851
+ end
852
+ end
853
+ def reset
854
+ @result = QEc2DescribeImageAttributeType.new
855
+ end
856
+ end
857
+
858
+ #-----------------------------------------------------------------
859
+ # PARSERS: Instances
860
+ #-----------------------------------------------------------------
861
+
862
+ class QEc2InstanceStateType # :nodoc:
863
+ attr_accessor :code
864
+ attr_accessor :name
865
+ end
866
+
867
+ class QEc2RunningInstancesItemType # :nodoc:
868
+ attr_accessor :instanceId
869
+ attr_accessor :imageId
870
+ attr_accessor :instanceState
871
+ attr_accessor :dnsName
872
+ attr_accessor :privateDnsName
873
+ attr_accessor :reason
874
+ attr_accessor :keyName
875
+ attr_accessor :amiLaunchIndex
876
+ end
877
+
878
+ class QEc2DescribeInstancesType # :nodoc:
879
+ attr_accessor :reservationId
880
+ attr_accessor :ownerId
881
+ attr_accessor :groupSet
882
+ attr_accessor :instancesSet
883
+ end
884
+
885
+ class QEc2DescribeInstancesParser < RightAWSParser # :nodoc:
886
+ def tagstart(name, attributes)
887
+ case name
888
+ when 'item'
889
+ if @xmlpath=='DescribeInstancesResponse/reservationSet'
890
+ @reservation = QEc2DescribeInstancesType.new
891
+ @reservation.groupSet = []
892
+ @reservation.instancesSet = []
893
+ elsif @xmlpath=='DescribeInstancesResponse/reservationSet/item/instancesSet'
894
+ @instance = QEc2RunningInstancesItemType.new
895
+ # the optional params (sometimes are missing and we dont want them to be nil)
896
+ @instance.reason = ''
897
+ @instance.dnsName = ''
898
+ @instance.privateDnsName = ''
899
+ @instance.amiLaunchIndex = ''
900
+ @instance.keyName = ''
901
+ @instance.instanceState = QEc2InstanceStateType.new
902
+ end
903
+ end
904
+ end
905
+ def tagend(name)
906
+ case name
907
+ when 'reservationId' ; @reservation.reservationId = @text
908
+ when 'ownerId' ; @reservation.ownerId = @text
909
+ when 'groupId' ; @reservation.groupSet << @text
910
+ when 'instanceId' ; @instance.instanceId = @text
911
+ when 'imageId' ; @instance.imageId = @text
912
+ when 'dnsName' ; @instance.dnsName = @text
913
+ when 'privateDnsName'; @instance.privateDnsName = @text
914
+ when 'reason' ; @instance.reason = @text
915
+ when 'keyName' ; @instance.keyName = @text
916
+ when 'amiLaunchIndex'; @instance.amiLaunchIndex = @text
917
+ when 'code' ; @instance.instanceState.code = @text
918
+ when 'name' ; @instance.instanceState.name = @text
919
+ when 'item'
920
+ if @xmlpath=='DescribeInstancesResponse/reservationSet/item/instancesSet'
921
+ @reservation.instancesSet << @instance
922
+ elsif @xmlpath=='DescribeInstancesResponse/reservationSet'
923
+ @result << @reservation
924
+ end
925
+ end
926
+ end
927
+ def reset
928
+ @result = []
929
+ end
930
+ end
931
+
932
+ class QEc2RunInstancesParser < RightAWSParser # :nodoc:
933
+ def tagstart(name, attributes)
934
+ case name
935
+ when 'RunInstancesResponse'
936
+ @reservation = QEc2DescribeInstancesType.new
937
+ @reservation.groupSet = []
938
+ @reservation.instancesSet = []
939
+ when 'item'
940
+ if @xmlpath == 'RunInstancesResponse/instancesSet'
941
+ @instance = QEc2RunningInstancesItemType.new
942
+ # the optional params (sometimes are missing and we dont want them to be nil)
943
+ @instance.reason = ''
944
+ @instance.dnsName = ''
945
+ @instance.privateDnsName = ''
946
+ @instance.amiLaunchIndex = ''
947
+ @instance.keyName = ''
948
+ @instance.instanceState = QEc2InstanceStateType.new
949
+ end
950
+ end
951
+ end
952
+ def tagend(name)
953
+ case name
954
+ when 'reservationId' ; @reservation.reservationId = @text
955
+ when 'ownerId' ; @reservation.ownerId = @text
956
+ when 'groupId' ; @reservation.groupSet << @text
957
+ when 'instanceId' ; @instance.instanceId = @text
958
+ when 'imageId' ; @instance.imageId = @text
959
+ when 'dnsName' ; @instance.dnsName = @text
960
+ when 'privateDnsName'; @instance.privateDnsName = @text
961
+ when 'reason' ; @instance.reason = @text
962
+ when 'keyName' ; @instance.keyName = @text
963
+ when 'amiLaunchIndex'; @instance.amiLaunchIndex = @text
964
+ when 'code' ; @instance.instanceState.code = @text
965
+ when 'name' ; @instance.instanceState.name = @text
966
+ when 'item'
967
+ @reservation.instancesSet << @instance if @xmlpath == 'RunInstancesResponse/instancesSet'
968
+ when 'RunInstancesResponse'; @result << @reservation
969
+ end
970
+ end
971
+ def reset
972
+ @result = []
973
+ end
974
+ end
975
+
976
+ class QEc2TerminateInstancesResponseInfoType # :nodoc:
977
+ attr_accessor :instanceId
978
+ attr_accessor :shutdownState
979
+ attr_accessor :previousState
980
+ end
981
+
982
+ class QEc2TerminateInstancesParser < RightAWSParser # :nodoc:
983
+ def tagstart(name, attributes)
984
+ if name == 'item'
985
+ @instance = QEc2TerminateInstancesResponseInfoType.new
986
+ @instance.shutdownState = QEc2InstanceStateType.new
987
+ @instance.previousState = QEc2InstanceStateType.new
988
+ end
989
+ end
990
+ def tagend(name)
991
+ case name
992
+ when 'instanceId' ; @instance.instanceId = @text
993
+ when 'item' ; @result << @instance
994
+ when 'code'
995
+ if @xmlpath == 'TerminateInstancesResponse/instancesSet/item/shutdownState'
996
+ @instance.shutdownState.code = @text
997
+ else @instance.previousState.code = @text end
998
+ when 'name'
999
+ if @xmlpath == 'TerminateInstancesResponse/instancesSet/item/shutdownState'
1000
+ @instance.shutdownState.name = @text
1001
+ else @instance.previousState.name = @text end
1002
+ end
1003
+ end
1004
+ def reset
1005
+ @result = []
1006
+ end
1007
+ end
1008
+
1009
+ #-----------------------------------------------------------------
1010
+ # PARSERS: Console
1011
+ #-----------------------------------------------------------------
1012
+
1013
+ class QEc2GetConsoleOutputResponseType # :nodoc:
1014
+ attr_accessor :instanceId
1015
+ attr_accessor :timestamp
1016
+ attr_accessor :output
1017
+ end
1018
+
1019
+ class QEc2GetConsoleOutputParser < RightAWSParser # :nodoc:
1020
+ def tagend(name)
1021
+ case name
1022
+ when 'instanceId' ; @result.instanceId = @text
1023
+ when 'timestamp' ; @result.timestamp = @text
1024
+ when 'output' ; @result.output = Base64.decode64 @text
1025
+ end
1026
+ end
1027
+ def reset
1028
+ @result = QEc2GetConsoleOutputResponseType.new
1029
+ end
1030
+ end
1031
+
1032
+ end
1033
+
1034
+ end