simple_aws 0.0.1a

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ doc
3
+ pkg
4
+ .bundle
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - rbx
6
+ - ruby-head
7
+ - ree
8
+ - jruby
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem "rake"
7
+ end
8
+
9
+ group :test do
10
+ gem "minitest", :require => false
11
+ gem "mocha", :require => false
12
+ end
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ SimpleAWS
2
+ =========
3
+
4
+ A thin, simple, forward compatible Ruby wrapper around the various Amazon AWS API.
5
+
6
+ What? Why?!
7
+ -----------
8
+
9
+ Another Ruby library to talk to Amazon Web Services? Why don't you just use fog
10
+ or aws or aws-sdk or right_aws or any of the myriad of others already in existence?
11
+ Why are you creating yet another one?
12
+
13
+ There's a simple, two part answer to this:
14
+
15
+ ### Complexity
16
+
17
+ It's in the name. *Simple* AWS. This library focuses on being the simplest possible way
18
+ to communicate with AWS. There's been a growing trend in the Ruby Library scene to abstract
19
+ and wrap everything possible in ActiveRecord-like objects so the library user doesn't
20
+ have to worry about the details of sending requests and parsing responses from AWS.
21
+ Some make it harder than others to get to the raw requests, but once down there you
22
+ run into the other problem these libraries have.
23
+
24
+ ### Forward Compatibility
25
+
26
+ Yes, not Backward, *Forward* compatibility. SimpleAWS is completely forward compatible to
27
+ any changes AWS makes to it's various APIs. It's well known that Amazon's AWS
28
+ is constantly changing, constantly being updated and added to. Unfortunately, there isn't
29
+ a library out there that lives this truth. Either parameters are hard coded, or response
30
+ values are hard coded, and for all of them the requests themselves are hard built into
31
+ the libraries, making it very hard to work with new API requests.
32
+
33
+ It's time for a library that evolves with Amazon AWS automatically and refuses to
34
+ get in your way. AWS's API is extremely well documented, consistent, and clean. The libraries
35
+ we use to interact with the API should match these truths as well.
36
+
37
+ How Simple?
38
+ -----------
39
+
40
+ Open a connection to the interface you want to talk to, and start calling methods.
41
+
42
+ ```ruby
43
+ ec2 = AWS::EC2.new(key, secret)
44
+ response = ec2.describe_instances
45
+ ```
46
+
47
+ If this looks familiar to other libraries, well, it's hard to get much simpler than this. Once
48
+ you move past no parameter methods though, the differences abound. What happens when Amazon
49
+ adds another parameter to DescribeInstances? SimpleAWS doesn't care, just start using it.
50
+
51
+ SimpleAWS is as light of a Ruby wrapper around the AWS APIs as possible. There are no
52
+ hard-coded parameters, no defined response types, you work directly with what the AWS
53
+ API says you get.
54
+
55
+ What SimpleAWS does do is to hide the communication complexities, the XML parsing, and
56
+ if you want to use it, some of the odd parameter systems AWS uses (PublicIp.n and the like).
57
+ On top of this, SimpleAWS works to ensure everything possible is as Ruby as possible. Methods
58
+ are underscore, the Response object can be queried using methods or a hash structure, and
59
+ parameter keys are converted to CamelCase strings as needed.
60
+
61
+ You're trying to use Amazon AWS, don't let libraries get in your way.
62
+
63
+ Project Info
64
+ ------------
65
+
66
+ Author: Jason Roelofs (https://github.com/jameskilton)
67
+
68
+ Source: https://github.com/jameskilton/simple_aws
69
+
70
+ Issues: https://github.com/jameskilton/simple_aws/issues
71
+
72
+ [![Travis CI Build Status](https://secure.travis-ci.org/jameskilton/simple_aws.png)](http://travis-ci.org/jameskilton/simple_aws)
73
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ desc "Run all tests"
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.pattern = "test/**/*_test.rb"
8
+ t.libs = ["lib", "test"]
9
+ end
data/lib/aws/api.rb ADDED
@@ -0,0 +1,82 @@
1
+ require 'aws/core/util'
2
+ require 'aws/core/connection'
3
+ require 'aws/core/request'
4
+
5
+ module AWS
6
+
7
+ ##
8
+ # Base class for all endpoint handler classes.
9
+ #
10
+ # See the list of AWS Endpoints for the values to use when
11
+ # implementing various APIs:
12
+ #
13
+ # http://docs.amazonwebservices.com/general/latest/gr/index.html?rande.html
14
+ ##
15
+ class API
16
+ class << self
17
+
18
+ ##
19
+ # Define the AWS endpoint for the API being wrapped.
20
+ ##
21
+ def endpoint(endpoint)
22
+ @endpoint = endpoint
23
+ end
24
+
25
+ ##
26
+ # Specify a default region for all requests for this API.
27
+ # This region will be used if no region is given to the
28
+ # constructor
29
+ ##
30
+ def default_region(region)
31
+ @default_region = region
32
+ end
33
+
34
+ ##
35
+ # Specify whether this API uses HTTPS for requests. If not set,
36
+ # the system will use HTTP. Some API endpoints are not available under
37
+ # HTTP and some are only HTTP.
38
+ ##
39
+ def use_https(value)
40
+ @use_https = value
41
+ end
42
+
43
+ ##
44
+ # Specify the AWS version of the API in question. This will be a date string.
45
+ ##
46
+ def version(version)
47
+ @version = version
48
+ end
49
+
50
+ end
51
+
52
+ attr_reader :access_key, :secret_key, :region, :version
53
+
54
+ ##
55
+ # Construct a new access object for the API in question.
56
+ # +access_key+ and +secret_key+ are as defined in AWS security standards.
57
+ # Use +region+ if you need to explicitly talk to a certain AWS region
58
+ ##
59
+ def initialize(access_key, secret_key, region = nil)
60
+ @access_key = access_key
61
+ @secret_key = secret_key
62
+
63
+ @region = region || self.class.instance_variable_get("@default_region")
64
+ @endpoint = self.class.instance_variable_get("@endpoint")
65
+ @use_https = self.class.instance_variable_get("@use_https")
66
+ @version = self.class.instance_variable_get("@version")
67
+ end
68
+
69
+ ##
70
+ # Get the full host name for the current API
71
+ ##
72
+ def uri
73
+ return @uri if @uri
74
+
75
+ @uri = @use_https ? "https" : "http"
76
+ @uri += "://#{@endpoint}"
77
+ @uri += ".#{@region}" if @region
78
+ @uri += ".amazonaws.com"
79
+ @uri
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,68 @@
1
+ require 'aws/core/util'
2
+ require 'aws/core/request'
3
+ require 'aws/core/connection'
4
+
5
+ module AWS
6
+ module CallTypes
7
+
8
+ ##
9
+ # Implement call handling to work with the ?Action param, signing the message
10
+ # according to that which is defined in EC2 and ELB.
11
+ ##
12
+ module ActionParam
13
+ ##
14
+ # For any undefined methods, try to convert them into valid AWS
15
+ # actions and return the results
16
+ ##
17
+ def method_missing(name, *args)
18
+ request = AWS::Request.new :post, self.uri, "/"
19
+ request.params["Action"] = AWS::Util.camelcase(name.to_s)
20
+
21
+ if args.any? && args.first.is_a?(Hash)
22
+ args.first.each do |key, value|
23
+ request.params[key] = value
24
+ end
25
+ end
26
+
27
+ connection = AWS::Connection.new
28
+ connection.call finish_and_sign_request(request)
29
+ end
30
+
31
+ protected
32
+
33
+ ##
34
+ # Build and sign the final request, as per the rules here:
35
+ # http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/index.html?using-query-api.html
36
+ ##
37
+ def finish_and_sign_request(request)
38
+ request.params.merge!({
39
+ "AWSAccessKeyId" => self.access_key,
40
+ "SignatureMethod" => "HmacSHA256",
41
+ "SignatureVersion" => "2",
42
+ "Timestamp" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
43
+ "Version" => self.version
44
+ })
45
+
46
+ request.params["Signature"] = Base64.encode64(sign_request(request.params.clone)).chomp
47
+
48
+ request
49
+ end
50
+
51
+ def sign_request(params)
52
+ list = params.map {|k, v| [k, Util.uri_escape(v.to_s)] }
53
+ list.sort! do |a, b|
54
+ if a[0] == "AWSAccessKeyId"
55
+ -1
56
+ else
57
+ a[0] <=> b[0]
58
+ end
59
+ end
60
+
61
+ host = self.uri.gsub(/^http[s]:\/\//,'')
62
+
63
+ to_sign = "POST\n#{host}\n/\n#{list.map {|p| p.join("=") }.join("&")}"
64
+ OpenSSL::HMAC.digest("sha256", self.secret_key, to_sign)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,32 @@
1
+ require 'httparty'
2
+ require 'openssl'
3
+
4
+ require 'aws/core/response'
5
+
6
+ module AWS
7
+
8
+ class HTTP
9
+ include HTTParty
10
+ format :xml
11
+ end
12
+
13
+ ##
14
+ # Handles all communication to and from AWS itself
15
+ ##
16
+ class Connection
17
+
18
+ ##
19
+ # Send an AWS::Request to AWS proper, returning an AWS::Response.
20
+ # Will raise if the request has an error
21
+ ##
22
+ def call(request)
23
+ AWS::Response.new(
24
+ HTTP.send(request.method,
25
+ request.uri,
26
+ :query => request.params
27
+ )
28
+ )
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,133 @@
1
+ module AWS
2
+
3
+ ##
4
+ # Defines all request information needed to run a request against an AWS API
5
+ #
6
+ # Requests need to know a number of attributes to work, including the host,
7
+ # path, the HTTP method, and any params or POST bodies. Most of this is
8
+ # straight forward through the constructor and setter methods defined below.
9
+ #
10
+ # One of the more interesting aspects of the AWS API are the indexed parameters.
11
+ # These are the parameters in the document defined as such:
12
+ #
13
+ # Filter.n.Name
14
+ # Filter.n.Value.m
15
+ #
16
+ # This class has special handling to facilitate building these parameters
17
+ # from regular Ruby Hashes and Arrays, but does not prevent you from specifying
18
+ # these parameters exactly as defined. For the example above, here are the two
19
+ # ways you can set these parameters:
20
+ #
21
+ # By yourself, filling in the +n+ and +m+ as you need:
22
+ #
23
+ # request.params.merge({
24
+ # "Filter.1.Name" => "domain",
25
+ # "Filter.1.Value" => "vpc",
26
+ # "Filter.2.Name" => "ids",
27
+ # "Filter.2.Value.1" => "i-1234",
28
+ # "Filter.2.Value.2" => "i-8902"
29
+ # })
30
+ #
31
+ # Or let Request handle the indexing and numbering for you:
32
+ #
33
+ # request.params["Filter"] = [
34
+ # {"Name" => "domain", "Value" => "vpc"},
35
+ # {"Name" => "ids", "Value" => ["i-1234", "i-8902"]}
36
+ # ]
37
+ #
38
+ # If you have just a single Filter, you don't need to wrap it in an array,
39
+ # Request will do that for you:
40
+ #
41
+ # request.params["Filter"] = {"Name" => "domain", "Value" => "vpc"}
42
+ #
43
+ # Straight arrays are handled as well:
44
+ #
45
+ # request.params["InstanceId"] = ["i-1234", "i-8970"]
46
+ #
47
+ # In an effort to make this library as transparent as possible when working
48
+ # directly with the AWS API, the keys of the hashes must be the values
49
+ # specified in the API, and the values must be Hashes, Arrays, or serializable
50
+ # values like Fixnum, Boolean, or String.
51
+ #
52
+ # A more detailed example can be found in test/aws/request_test.rb where you'll
53
+ # see how to use many levels of nesting to build your AWS request.
54
+ ##
55
+ class Request
56
+
57
+ class Params < Hash
58
+
59
+ def []=(key, value)
60
+ case value
61
+ when Array
62
+ process_array key, value
63
+ when Hash
64
+ process_array key, [value]
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ def process_array(base_key, array_in)
73
+ array_in.each_with_index do |entry, index|
74
+ entry_key = "#{base_key}.#{index + 1}"
75
+ case entry
76
+ when Hash
77
+ process_hash entry_key, entry
78
+ else
79
+ self[entry_key] = entry
80
+ end
81
+ end
82
+ end
83
+
84
+ def process_hash(base_key, entry)
85
+ entry.each do |inner_key, inner_value|
86
+ full_inner_key = "#{base_key}.#{inner_key}"
87
+ case inner_value
88
+ when Array
89
+ process_array full_inner_key, inner_value
90
+ else
91
+ self[full_inner_key] = inner_value
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ ##
99
+ # HTTP method this Request will use (:get, :post, :put, :delete)
100
+ ##
101
+ attr_reader :method
102
+
103
+ ##
104
+ # Host and Path of the URI this Request will be using
105
+ ##
106
+ attr_reader :host, :path
107
+
108
+ ##
109
+ # Hash of parameters to pass in this Request. See top-level
110
+ # documentation for any special handling of types
111
+ ##
112
+ attr_reader :params
113
+
114
+ ##
115
+ # Set up a new Request for the given +host+ and +path+ using the given
116
+ # http +method+ (:get, :post, :put, :delete).
117
+ ##
118
+ def initialize(method, host, path)
119
+ @method = method
120
+ @host = host
121
+ @path = path
122
+ @params = Params.new
123
+ end
124
+
125
+ ##
126
+ # Build up the full URI
127
+ ##
128
+ def uri
129
+ "#{host}#{path}"
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,231 @@
1
+ require 'aws/core/util'
2
+
3
+ module AWS
4
+
5
+ class UnsuccessfulResponse < RuntimeError
6
+ attr_reader :code
7
+ attr_reader :error_type
8
+ attr_reader :error_message
9
+
10
+ def initialize(code, error_type, error_message)
11
+ super "#{error_type} (#{code}): #{error_message}"
12
+ @code = code
13
+ @error_type = error_type
14
+ @error_message = error_message
15
+ end
16
+ end
17
+
18
+ class UnknownErrorResponse < RuntimeError
19
+ def initialize(body)
20
+ super "Unable to parse error code from #{body.inspect}"
21
+ end
22
+ end
23
+
24
+ ##
25
+ # Wrapper object for all responses from AWS. This class gives
26
+ # a lot of leeway to how you access the response object.
27
+ # You can access the response directly through it's Hash representation,
28
+ # which is a direct mapping from the raw XML returned from AWS.
29
+ #
30
+ # You can also use ruby methods. This object will convert those methods
31
+ # in ruby_standard into appropriate keys (camelCase) and look for them
32
+ # in the hash. This can be done at any depth.
33
+ #
34
+ # This class tries not to be too magical to ensure that
35
+ # it never gets in the way. All nested objects are queryable like their
36
+ # parents are, and all sets and arrays are found and accessible through
37
+ # your typical Enumerable interface.
38
+ #
39
+ # The starting point of the Response querying will vary according to the structure
40
+ # returned by the AWS API in question. For some APIs, like EC2, the response is
41
+ # a relatively flat:
42
+ #
43
+ # <DataRequestResponse>
44
+ # <requestId>...</requestId>
45
+ # <dataRequested>
46
+ # ...
47
+ # </dataRequested>
48
+ # </DataRequestResponse>
49
+ #
50
+ # In this case, your querying will start inside of <DataRequestResponse>, ala the first
51
+ # method you'll probably call is +data_requested+. For other APIs, the response
52
+ # object is a little deeper and looks like this:
53
+ #
54
+ # <DataRequestResponse>
55
+ # <DataRequestedResult>
56
+ # <DataRequested>
57
+ # ...
58
+ # </DataRequested>
59
+ # </DataRequestedResult>
60
+ # <ResponseMetadata>
61
+ # <RequestId>...</RequestId>
62
+ # </ResponseMetadata>
63
+ # </DataRequestResponse>
64
+ #
65
+ # For these response structures, your query will start inside of <DataRequestedResult>,
66
+ # ala your first method call will be +data_requested+. To get access to the request id of
67
+ # both of these structures, simply use #request_id on the base response. You'll also
68
+ # notice the case differences of the XML tags, this class tries to ensure that case doesn't
69
+ # matter when you're querying with methods. If you're using raw hash access then yes the
70
+ # case of the keys in question need to match.
71
+ #
72
+ # This class does ensure that any collection is always an Array, given that
73
+ # when AWS returns a single item in a collection, the xml -> hash parser gives a
74
+ # single hash back instead of an array. This class will also look for
75
+ # array indicators from AWS, like <item> or <member> and squash them.
76
+ #
77
+ # If AWS returns an error code, instead of getting a Response back the library
78
+ # will instead throw an UnsuccessfulResponse error with the pertinent information.
79
+ ##
80
+ class Response
81
+
82
+ # Inner proxy class that handles converting ruby methods
83
+ # into keys found in the underlying Hash.
84
+ class ResponseProxy
85
+ include Enumerable
86
+
87
+ TO_SQUASH = %w(item member)
88
+
89
+ def initialize(local_root)
90
+ first_key = local_root.keys.first
91
+ if local_root.keys.length == 1 && TO_SQUASH.include?(first_key)
92
+ # Ensure squash key is ignored and it's children are always
93
+ # turned into an array.
94
+ @local_root = [local_root[first_key]].flatten.map do |entry|
95
+ ResponseProxy.new entry
96
+ end
97
+ else
98
+ @local_root = local_root
99
+ end
100
+ end
101
+
102
+ def [](key_or_idx)
103
+ value_or_proxy @local_root[key_or_idx]
104
+ end
105
+
106
+ ##
107
+ # Get all keys at the current depth of the Response object.
108
+ # This method will raise a NoMethodError if the current
109
+ # depth is an array.
110
+ ##
111
+ def keys
112
+ @local_root.keys
113
+ end
114
+
115
+ def length
116
+ @local_root.length
117
+ end
118
+
119
+ def each(&block)
120
+ @local_root.each(&block)
121
+ end
122
+
123
+ def method_missing(name, *args)
124
+ if key = key_matching(name)
125
+ value_or_proxy @local_root[key]
126
+ else
127
+ super
128
+ end
129
+ end
130
+
131
+ protected
132
+
133
+ def key_matching(name)
134
+ return nil if @local_root.is_a? Array
135
+
136
+ lower_base_aws_name = AWS::Util.camelcase name.to_s, :lower
137
+ upper_base_aws_name = AWS::Util.camelcase name.to_s
138
+
139
+ keys = @local_root.keys
140
+
141
+ if keys.include? lower_base_aws_name
142
+ lower_base_aws_name
143
+ elsif keys.include? upper_base_aws_name
144
+ upper_base_aws_name
145
+ end
146
+ end
147
+
148
+ def value_or_proxy(value)
149
+ if value.is_a?(Hash) || value.is_a?(Array)
150
+ ResponseProxy.new value
151
+ else
152
+ value
153
+ end
154
+ end
155
+ end
156
+
157
+ ##
158
+ # The raw parsed response body in Hash format
159
+ ##
160
+ attr_reader :body
161
+
162
+ def initialize(http_response)
163
+ if !http_response.success?
164
+ error = parse_error_from http_response.parsed_response
165
+ raise UnsuccessfulResponse.new(
166
+ http_response.code,
167
+ error["Code"],
168
+ error["Message"]
169
+ )
170
+ end
171
+
172
+ @body = http_response.parsed_response
173
+
174
+ inner = @body[@body.keys.first]
175
+ response_root =
176
+ if result_key = inner.keys.find {|k| k =~ /Result$/}
177
+ inner[result_key]
178
+ else
179
+ inner
180
+ end
181
+
182
+ @request_root = ResponseProxy.new response_root
183
+ end
184
+
185
+ ##
186
+ # Direct access to the request body's hash.
187
+ # This works on the first level down in the AWS response, bypassing
188
+ # the root element of the returned XML so you can work directly in the
189
+ # attributes that matter
190
+ ##
191
+ def [](key)
192
+ @request_root[key]
193
+ end
194
+
195
+ ##
196
+ # Delegate first-level method calls to the root Proxy object
197
+ ##
198
+ def method_missing(name, *args)
199
+ @request_root.send(name, *args)
200
+ end
201
+
202
+ ##
203
+ # Get the request ID from this response. Works on all known AWS response formats.
204
+ # Some AWS APIs don't give a request id, such as CloudFront. For responses that
205
+ # do not have a request id, this method returns nil.
206
+ ##
207
+ def request_id
208
+ if metadata = @body[@body.keys.first]["ResponseMetadata"]
209
+ metadata["RequestId"]
210
+ elsif id = @body[@body.keys.first]["requestId"]
211
+ id
212
+ else
213
+ nil
214
+ end
215
+ end
216
+
217
+ protected
218
+
219
+ def parse_error_from(body)
220
+ if body.has_key? "ErrorResponse"
221
+ body["ErrorResponse"]["Error"]
222
+ elsif body.has_key? "Response"
223
+ body["Response"]["Errors"]["Error"]
224
+ else
225
+ raise UnknownErrorResponse.new body
226
+ end
227
+ end
228
+
229
+ end
230
+
231
+ end
@@ -0,0 +1,32 @@
1
+ module AWS
2
+ ##
3
+ # Collection of helper methods used in the library
4
+ ##
5
+ module Util
6
+
7
+ ##
8
+ # Simpler version of ActiveSupport's camelize
9
+ ##
10
+ def self.camelcase(string, lower_first_char = false)
11
+ if lower_first_char
12
+ string[0,1].downcase + camelcase(string)[1..-1]
13
+ else
14
+ string.split(/_/).map{ |word| word.capitalize }.join('')
15
+ end
16
+ end
17
+
18
+
19
+ # AWS URI escaping, as implemented by Fog
20
+ def self.uri_escape(string)
21
+ # Quick hack for already escaped string, don't escape again
22
+ # I don't think any requests require a % in a parameter, but if
23
+ # there is one I'll need to rethink this
24
+ return string if string =~ /%/
25
+
26
+ string.gsub(/([^a-zA-Z0-9_.\-~]+)/) {
27
+ "%" + $1.unpack("H2" * $1.bytesize).join("%").upcase
28
+ }
29
+ end
30
+
31
+ end
32
+ end