miasma-aws 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|