simple_aws 0.0.1a

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.
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