miasma-aws 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE +13 -0
- data/README.md +20 -0
- data/lib/miasma-aws.rb +2 -0
- data/lib/miasma-aws/version.rb +4 -0
- data/lib/miasma/contrib/aws.rb +444 -0
- data/lib/miasma/contrib/aws/auto_scale.rb +86 -0
- data/lib/miasma/contrib/aws/compute.rb +113 -0
- data/lib/miasma/contrib/aws/load_balancer.rb +187 -0
- data/lib/miasma/contrib/aws/orchestration.rb +350 -0
- data/lib/miasma/contrib/aws/storage.rb +405 -0
- data/miasma-aws.gemspec +15 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a3f6e18c7aa052992e9ffd65f0ce75d61b428704
|
4
|
+
data.tar.gz: ed79d1ba6dd4b90239b86a526a68e1d8946c01d1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 79dbeffe45430e68c1e8af17988428e96e01a0e49345ac0ab1bb259ae021e86bfc2f19d8a0ed05b509281d74a3398861ad3fc55236a097852fe8c129c5d880f3
|
7
|
+
data.tar.gz: ebdaebefc0b15e64cb0609f0da662e7ea56c49cb2d837c4a6ae2228ef1763244160a0b55b466ef6f403e475f60dde534fc860883d4fab45ce314be07520c63ff
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2014 Chris Roberts
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Miasma AWS
|
2
|
+
|
3
|
+
AWS API plugin for the miasma cloud library
|
4
|
+
|
5
|
+
## Current support matrix
|
6
|
+
|
7
|
+
|Model |Create|Read|Update|Delete|
|
8
|
+
|--------------|------|----|------|------|
|
9
|
+
|AutoScale | X | X | | |
|
10
|
+
|BlockStorage | | | | |
|
11
|
+
|Compute | X | X | | X |
|
12
|
+
|DNS | | | | |
|
13
|
+
|LoadBalancer | X | X | X | X |
|
14
|
+
|Network | | | | |
|
15
|
+
|Orchestration | X | X | X | X |
|
16
|
+
|Queues | | | | |
|
17
|
+
|Storage | X | X | X | X |
|
18
|
+
|
19
|
+
## Info
|
20
|
+
* Repository: https://github.com/miasma-rb/miasma-aws
|
data/lib/miasma-aws.rb
ADDED
@@ -0,0 +1,444 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
require 'miasma/utils/smash'
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
module Miasma
|
8
|
+
module Contrib
|
9
|
+
# Core API for AWS access
|
10
|
+
class AwsApiCore
|
11
|
+
|
12
|
+
module RequestUtils
|
13
|
+
|
14
|
+
# Fetch all results when tokens are being used
|
15
|
+
# for paging results
|
16
|
+
#
|
17
|
+
# @param next_token [String]
|
18
|
+
# @param result_key [Array<String, Symbol>] path to result
|
19
|
+
# @yield block to perform request
|
20
|
+
# @yieldparam options [Hash] request parameters (token information)
|
21
|
+
# @return [Array]
|
22
|
+
def all_result_pages(next_token, *result_key, &block)
|
23
|
+
list = []
|
24
|
+
options = next_token ? Smash.new('NextToken' => next_token) : Smash.new
|
25
|
+
result = block.call(options)
|
26
|
+
content = result.get(*result_key.dup)
|
27
|
+
if(content.is_a?(Array))
|
28
|
+
list += content
|
29
|
+
else
|
30
|
+
list << content
|
31
|
+
end
|
32
|
+
set = result.get(*result_key.slice(0, 3))
|
33
|
+
if(set && set['NextToken'])
|
34
|
+
list += all_result_pages(set['NextToken'], *result_key, &block)
|
35
|
+
end
|
36
|
+
list.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] current time ISO8601 format
|
42
|
+
def self.time_iso8601
|
43
|
+
Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
|
44
|
+
end
|
45
|
+
|
46
|
+
# HMAC helper class
|
47
|
+
class Hmac
|
48
|
+
|
49
|
+
# @return [OpenSSL::Digest]
|
50
|
+
attr_reader :digest
|
51
|
+
# @return [String] secret key
|
52
|
+
attr_reader :key
|
53
|
+
|
54
|
+
# Create new HMAC helper
|
55
|
+
#
|
56
|
+
# @param kind [String] digest type (sha1, sha256, sha512, etc)
|
57
|
+
# @param key [String] secret key
|
58
|
+
# @return [self]
|
59
|
+
def initialize(kind, key)
|
60
|
+
@digest = OpenSSL::Digest.new(kind)
|
61
|
+
@key = key
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String]
|
65
|
+
def to_s
|
66
|
+
"Hmac#{digest.name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Generate the hexdigest of the content
|
70
|
+
#
|
71
|
+
# @param content [String] content to digest
|
72
|
+
# @return [String] hashed result
|
73
|
+
def hexdigest_of(content)
|
74
|
+
digest << content
|
75
|
+
hash = digest.hexdigest
|
76
|
+
digest.reset
|
77
|
+
hash
|
78
|
+
end
|
79
|
+
|
80
|
+
# Sign the given data
|
81
|
+
#
|
82
|
+
# @param data [String]
|
83
|
+
# @param key_override [Object]
|
84
|
+
# @return [Object] signature
|
85
|
+
def sign(data, key_override=nil)
|
86
|
+
result = OpenSSL::HMAC.digest(digest, key_override || key, data)
|
87
|
+
digest.reset
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sign the given data and return hexdigest
|
92
|
+
#
|
93
|
+
# @param data [String]
|
94
|
+
# @param key_override [Object]
|
95
|
+
# @return [String] hex encoded signature
|
96
|
+
def hex_sign(data, key_override=nil)
|
97
|
+
result = OpenSSL::HMAC.hexdigest(digest, key_override || key, data)
|
98
|
+
digest.reset
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
# Base signature class
|
105
|
+
class Signature
|
106
|
+
|
107
|
+
# Create new instance
|
108
|
+
def initialize(*args)
|
109
|
+
raise NotImplementedError.new 'This class should not be used directly!'
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generate the signature
|
113
|
+
#
|
114
|
+
# @param http_method [Symbol] HTTP request method
|
115
|
+
# @param path [String] request path
|
116
|
+
# @param opts [Hash] request options
|
117
|
+
# @return [String] signature
|
118
|
+
def generate(http_method, path, opts={})
|
119
|
+
raise NotImplementedError
|
120
|
+
end
|
121
|
+
|
122
|
+
# URL string escape compatible with AWS requirements
|
123
|
+
#
|
124
|
+
# @param string [String] string to escape
|
125
|
+
# @return [String] escaped string
|
126
|
+
def safe_escape(string)
|
127
|
+
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
|
128
|
+
'%' << $1.unpack('H2' * $1.bytesize).join('%').upcase
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
# AWS signature version 4
|
135
|
+
class SignatureV4 < Signature
|
136
|
+
|
137
|
+
# @return [Hmac]
|
138
|
+
attr_reader :hmac
|
139
|
+
# @return [String] access key
|
140
|
+
attr_reader :access_key
|
141
|
+
# @return [String] region
|
142
|
+
attr_reader :region
|
143
|
+
# @return [String] service
|
144
|
+
attr_reader :service
|
145
|
+
|
146
|
+
# Create new signature generator
|
147
|
+
#
|
148
|
+
# @param access_key [String]
|
149
|
+
# @param secret_key [String]
|
150
|
+
# @param region [String]
|
151
|
+
# @param service [String]
|
152
|
+
# @return [self]
|
153
|
+
def initialize(access_key, secret_key, region, service)
|
154
|
+
@hmac = Hmac.new('sha256', secret_key)
|
155
|
+
@access_key = access_key
|
156
|
+
@region = region
|
157
|
+
@service = service
|
158
|
+
end
|
159
|
+
|
160
|
+
# Generate the signature string for AUTH
|
161
|
+
#
|
162
|
+
# @param http_method [Symbol] HTTP request method
|
163
|
+
# @param path [String] request path
|
164
|
+
# @param opts [Hash] request options
|
165
|
+
# @return [String] signature
|
166
|
+
def generate(http_method, path, opts)
|
167
|
+
signature = generate_signature(http_method, path, opts)
|
168
|
+
"#{algorithm} Credential=#{access_key}/#{credential_scope}, SignedHeaders=#{signed_headers(opts[:headers])}, Signature=#{signature}"
|
169
|
+
end
|
170
|
+
|
171
|
+
# Generate URL with signed params
|
172
|
+
#
|
173
|
+
# @param http_method [Symbol] HTTP request method
|
174
|
+
# @param path [String] request path
|
175
|
+
# @param opts [Hash] request options
|
176
|
+
# @return [String] signature
|
177
|
+
def generate_url(http_method, path, opts)
|
178
|
+
opts[:params].merge!(
|
179
|
+
Smash.new(
|
180
|
+
'X-Amz-SignedHeaders' => signed_headers(opts[:headers]),
|
181
|
+
'X-Amz-Algorithm' => algorithm,
|
182
|
+
'X-Amz-Credential' => "#{access_key}/#{credential_scope}"
|
183
|
+
)
|
184
|
+
)
|
185
|
+
signature = generate_signature(http_method, path, opts.merge(:body => 'UNSIGNED-PAYLOAD'))
|
186
|
+
params = opts[:params].merge('X-Amz-Signature' => signature)
|
187
|
+
"https://#{opts[:headers]['Host']}/#{path}?#{canonical_query(params)}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Generate the signature
|
191
|
+
#
|
192
|
+
# @param http_method [Symbol] HTTP request method
|
193
|
+
# @param path [String] request path
|
194
|
+
# @param opts [Hash] request options
|
195
|
+
# @return [String] signature
|
196
|
+
def generate_signature(http_method, path, opts)
|
197
|
+
to_sign = [
|
198
|
+
algorithm,
|
199
|
+
AwsApiCore.time_iso8601,
|
200
|
+
credential_scope,
|
201
|
+
hashed_canonical_request(
|
202
|
+
can_req = build_canonical_request(http_method, path, opts)
|
203
|
+
)
|
204
|
+
].join("\n")
|
205
|
+
signature = sign_request(to_sign)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Sign the request
|
209
|
+
#
|
210
|
+
# @param request [String] request to sign
|
211
|
+
# @return [String] signature
|
212
|
+
def sign_request(request)
|
213
|
+
key = hmac.sign(
|
214
|
+
'aws4_request',
|
215
|
+
hmac.sign(
|
216
|
+
service,
|
217
|
+
hmac.sign(
|
218
|
+
region,
|
219
|
+
hmac.sign(
|
220
|
+
Time.now.utc.strftime('%Y%m%d'),
|
221
|
+
"AWS4#{hmac.key}"
|
222
|
+
)
|
223
|
+
)
|
224
|
+
)
|
225
|
+
)
|
226
|
+
hmac.hex_sign(request, key)
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [String] signature algorithm
|
230
|
+
def algorithm
|
231
|
+
'AWS4-HMAC-SHA256'
|
232
|
+
end
|
233
|
+
|
234
|
+
# @return [String] credential scope for request
|
235
|
+
def credential_scope
|
236
|
+
[
|
237
|
+
Time.now.utc.strftime('%Y%m%d'),
|
238
|
+
region,
|
239
|
+
service,
|
240
|
+
'aws4_request'
|
241
|
+
].join('/')
|
242
|
+
end
|
243
|
+
|
244
|
+
# Generate the hash of the canonical request
|
245
|
+
#
|
246
|
+
# @param request [String] canonical request string
|
247
|
+
# @return [String] hashed canonical request
|
248
|
+
def hashed_canonical_request(request)
|
249
|
+
hmac.hexdigest_of(request)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Build the canonical request string used for signing
|
253
|
+
#
|
254
|
+
# @param http_method [Symbol] HTTP request method
|
255
|
+
# @param path [String] request path
|
256
|
+
# @param opts [Hash] request options
|
257
|
+
# @return [String] canonical request string
|
258
|
+
def build_canonical_request(http_method, path, opts)
|
259
|
+
unless(path.start_with?('/'))
|
260
|
+
path = "/#{path}"
|
261
|
+
end
|
262
|
+
[
|
263
|
+
http_method.to_s.upcase,
|
264
|
+
path,
|
265
|
+
canonical_query(opts[:params]),
|
266
|
+
canonical_headers(opts[:headers]),
|
267
|
+
signed_headers(opts[:headers]),
|
268
|
+
canonical_payload(opts)
|
269
|
+
].join("\n")
|
270
|
+
end
|
271
|
+
|
272
|
+
# Build the canonical query string used for signing
|
273
|
+
#
|
274
|
+
# @param params [Hash] query params
|
275
|
+
# @return [String] canonical query string
|
276
|
+
def canonical_query(params)
|
277
|
+
params ||= {}
|
278
|
+
params = Hash[params.sort_by(&:first)]
|
279
|
+
query = params.map do |key, value|
|
280
|
+
"#{safe_escape(key)}=#{safe_escape(value)}"
|
281
|
+
end.join('&')
|
282
|
+
end
|
283
|
+
|
284
|
+
# Build the canonical header string used for signing
|
285
|
+
#
|
286
|
+
# @param headers [Hash] request headers
|
287
|
+
# @return [String] canonical headers string
|
288
|
+
def canonical_headers(headers)
|
289
|
+
headers ||= {}
|
290
|
+
headers = Hash[headers.sort_by(&:first)]
|
291
|
+
headers.map do |key, value|
|
292
|
+
[key.downcase, value.chomp].join(':')
|
293
|
+
end.join("\n") << "\n"
|
294
|
+
end
|
295
|
+
|
296
|
+
# List of headers included in signature
|
297
|
+
#
|
298
|
+
# @param headers [Hash] request headers
|
299
|
+
# @return [String] header list
|
300
|
+
def signed_headers(headers)
|
301
|
+
headers ||= {}
|
302
|
+
headers.sort_by(&:first).map(&:first).
|
303
|
+
map(&:downcase).join(';')
|
304
|
+
end
|
305
|
+
|
306
|
+
# Build the canonical payload string used for signing
|
307
|
+
#
|
308
|
+
# @param options [Hash] request options
|
309
|
+
# @return [String] body checksum
|
310
|
+
def canonical_payload(options)
|
311
|
+
body = options.fetch(:body, '')
|
312
|
+
if(options[:json])
|
313
|
+
body = MultiJson.dump(options[:json])
|
314
|
+
elsif(options[:form])
|
315
|
+
body = URI.encode_www_form(options[:form])
|
316
|
+
end
|
317
|
+
if(body == 'UNSIGNED-PAYLOAD')
|
318
|
+
body
|
319
|
+
else
|
320
|
+
hmac.hexdigest_of(body)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
module ApiCommon
|
327
|
+
|
328
|
+
def self.included(klass)
|
329
|
+
klass.class_eval do
|
330
|
+
attribute :aws_access_key_id, String, :required => true
|
331
|
+
attribute :aws_secret_access_key, String, :required => true
|
332
|
+
attribute :aws_region, String, :required => true
|
333
|
+
attribute :aws_host, String
|
334
|
+
attribute :aws_bucket_region, String
|
335
|
+
|
336
|
+
# @return [Contrib::AwsApiCore::SignatureV4]
|
337
|
+
attr_reader :signer
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Build new API for specified type using current provider / creds
|
342
|
+
#
|
343
|
+
# @param type [Symbol] api type
|
344
|
+
# @return [Api]
|
345
|
+
def api_for(type)
|
346
|
+
memoize(type) do
|
347
|
+
creds = attributes.dup
|
348
|
+
creds.delete(:aws_host)
|
349
|
+
Miasma.api(
|
350
|
+
Smash.new(
|
351
|
+
:type => type,
|
352
|
+
:provider => provider,
|
353
|
+
:credentials => creds
|
354
|
+
)
|
355
|
+
)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Setup for API connections
|
360
|
+
def connect
|
361
|
+
unless(aws_host)
|
362
|
+
self.aws_host = [
|
363
|
+
self.class::API_SERVICE.downcase,
|
364
|
+
aws_region,
|
365
|
+
'amazonaws.com'
|
366
|
+
].join('.')
|
367
|
+
end
|
368
|
+
@signer = Contrib::AwsApiCore::SignatureV4.new(
|
369
|
+
aws_access_key_id, aws_secret_access_key, aws_region, self.class::API_SERVICE
|
370
|
+
)
|
371
|
+
end
|
372
|
+
|
373
|
+
# @return [String] custom escape for aws compat
|
374
|
+
def uri_escape(string)
|
375
|
+
signer.safe_escape(string)
|
376
|
+
end
|
377
|
+
|
378
|
+
# @return [HTTP] connection for requests (forces headers)
|
379
|
+
def connection
|
380
|
+
super.with_headers(
|
381
|
+
'Host' => aws_host,
|
382
|
+
'X-Amz-Date' => Contrib::AwsApiCore.time_iso8601
|
383
|
+
)
|
384
|
+
end
|
385
|
+
|
386
|
+
# @return [String] endpoint for request
|
387
|
+
def endpoint
|
388
|
+
"https://#{aws_host}"
|
389
|
+
end
|
390
|
+
|
391
|
+
# Override to inject signature
|
392
|
+
#
|
393
|
+
# @param connection [HTTP]
|
394
|
+
# @param http_method [Symbol]
|
395
|
+
# @param request_args [Array]
|
396
|
+
# @return [HTTP::Response]
|
397
|
+
# @note if http_method is :post, params will be automatically
|
398
|
+
# removed and placed into :form
|
399
|
+
def make_request(connection, http_method, request_args)
|
400
|
+
dest, options = request_args
|
401
|
+
path = URI.parse(dest).path
|
402
|
+
options = options ? options.to_smash : Smash.new
|
403
|
+
options[:params] = options.fetch(:params, Smash.new).to_smash.deep_merge('Version' => self.class::API_VERSION)
|
404
|
+
if(http_method.to_sym == :post)
|
405
|
+
if(options[:form])
|
406
|
+
options[:form].merge(options.delete(:params))
|
407
|
+
else
|
408
|
+
options[:form] = options.delete(:params)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
update_request(connection, options)
|
412
|
+
signature = signer.generate(
|
413
|
+
http_method, path, options.merge(
|
414
|
+
Smash.new(
|
415
|
+
:headers => Smash[
|
416
|
+
connection.default_headers.to_a
|
417
|
+
]
|
418
|
+
)
|
419
|
+
)
|
420
|
+
)
|
421
|
+
options = Hash[options.map{|k,v|[k.to_sym,v]}]
|
422
|
+
connection.auth(signature).send(http_method, dest, options)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Simple callback to allow request option adjustments prior to
|
426
|
+
# signature calculation
|
427
|
+
#
|
428
|
+
# @param opts [Smash] request options
|
429
|
+
# @return [TrueClass]
|
430
|
+
def update_request(con, opts)
|
431
|
+
true
|
432
|
+
end
|
433
|
+
|
434
|
+
end
|
435
|
+
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
Models::Compute.autoload :Aws, 'miasma/contrib/aws/compute'
|
440
|
+
Models::LoadBalancer.autoload :Aws, 'miasma/contrib/aws/load_balancer'
|
441
|
+
Models::AutoScale.autoload :Aws, 'miasma/contrib/aws/auto_scale'
|
442
|
+
Models::Orchestration.autoload :Aws, 'miasma/contrib/aws/orchestration'
|
443
|
+
Models::Storage.autoload :Aws, 'miasma/contrib/aws/storage'
|
444
|
+
end
|