right_aws 1.1.0

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