miasma-aws 0.3.4 → 0.3.6
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +4 -0
- data/lib/miasma-aws/api.rb +1 -0
- data/lib/miasma-aws/api/iam.rb +3 -2
- data/lib/miasma-aws/api/sts.rb +3 -2
- data/lib/miasma-aws/version.rb +1 -1
- data/lib/miasma/contrib/aws.rb +133 -38
- data/lib/miasma/contrib/aws/auto_scale.rb +10 -5
- data/lib/miasma/contrib/aws/compute.rb +33 -15
- data/lib/miasma/contrib/aws/load_balancer.rb +26 -9
- data/lib/miasma/contrib/aws/orchestration.rb +47 -23
- data/lib/miasma/contrib/aws/storage.rb +9 -5
- data/miasma-aws.gemspec +3 -3
- metadata +15 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: caa36177f0ea2511cec15e3f84b55ab542845255
|
4
|
+
data.tar.gz: c2e70dcbce33f6514fd110a2d1e90f2baf148675
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7aff9a89bb222d9a3a247d167a3536759b4008aa1bb6fc83ab5baf536292452a7524319cbd113ea998c2be0bf3ed92833af6336f7d58f37f375b2e0ed46546c8
|
7
|
+
data.tar.gz: 488ba3c0126a6d38795edd6d78f5e060b89370c9c34ababcd1f5996cff2bab4e428a924d0ecc947e0756e7686d8357bb3f4c60bbf02a357c1c717f866ce5df0d
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -33,6 +33,10 @@ Miasma.api(
|
|
33
33
|
|
34
34
|
* `aws_iam_instance_profile` - Extract and use instance IAM credentials
|
35
35
|
|
36
|
+
### ECS task related attributes
|
37
|
+
|
38
|
+
* `aws_ecs_task_profile` - Extract and use ECS task IAM credentials
|
39
|
+
|
36
40
|
### Secure Token Service related:
|
37
41
|
|
38
42
|
* `aws_sts_token` - Set STS token to use with current key ID and secret
|
data/lib/miasma-aws/api.rb
CHANGED
data/lib/miasma-aws/api/iam.rb
CHANGED
@@ -4,12 +4,13 @@ module Miasma
|
|
4
4
|
module Contrib
|
5
5
|
module Aws
|
6
6
|
module Api
|
7
|
+
# IAM helper class
|
7
8
|
class Iam < Miasma::Types::Api
|
8
9
|
|
9
10
|
# Service name of the API
|
10
|
-
API_SERVICE = 'iam'
|
11
|
+
API_SERVICE = 'iam'.freeze
|
11
12
|
# Supported version of the IAM API
|
12
|
-
API_VERSION = '2010-05-08'
|
13
|
+
API_VERSION = '2010-05-08'.freeze
|
13
14
|
|
14
15
|
include Contrib::AwsApiCore::ApiCommon
|
15
16
|
include Contrib::AwsApiCore::RequestUtils
|
data/lib/miasma-aws/api/sts.rb
CHANGED
@@ -4,12 +4,13 @@ module Miasma
|
|
4
4
|
module Contrib
|
5
5
|
module Aws
|
6
6
|
module Api
|
7
|
+
# STS helper class
|
7
8
|
class Sts < Miasma::Types::Api
|
8
9
|
|
9
10
|
# Service name of the API
|
10
|
-
API_SERVICE = 'sts'
|
11
|
+
API_SERVICE = 'sts'.freeze
|
11
12
|
# Supported version of the STS API
|
12
|
-
API_VERSION = '2011-06-15'
|
13
|
+
API_VERSION = '2011-06-15'.freeze
|
13
14
|
|
14
15
|
include Contrib::AwsApiCore::ApiCommon
|
15
16
|
include Contrib::AwsApiCore::RequestUtils
|
data/lib/miasma-aws/version.rb
CHANGED
data/lib/miasma/contrib/aws.rb
CHANGED
@@ -4,14 +4,17 @@ require 'miasma/utils/smash'
|
|
4
4
|
require 'time'
|
5
5
|
require 'openssl'
|
6
6
|
|
7
|
+
# Miasma
|
7
8
|
module Miasma
|
8
9
|
module Contrib
|
10
|
+
# AWS API implementations
|
9
11
|
module Aws
|
10
12
|
autoload :Api, 'miasma-aws/api'
|
11
13
|
end
|
12
14
|
# Core API for AWS access
|
13
15
|
class AwsApiCore
|
14
16
|
|
17
|
+
# Utility methods for API requests
|
15
18
|
module RequestUtils
|
16
19
|
|
17
20
|
# Fetch all results when tokens are being used
|
@@ -34,6 +37,11 @@ module Miasma
|
|
34
37
|
end
|
35
38
|
set = result.get(*result_key.slice(0, 3))
|
36
39
|
if(set.is_a?(Hash) && set['NextToken'])
|
40
|
+
[content].flatten.compact.each do |item|
|
41
|
+
if(item.is_a?(Hash))
|
42
|
+
item['NextToken'] = set['NextToken']
|
43
|
+
end
|
44
|
+
end
|
37
45
|
list += all_result_pages(set['NextToken'], *result_key, &block)
|
38
46
|
end
|
39
47
|
list.compact
|
@@ -127,8 +135,8 @@ module Miasma
|
|
127
135
|
# @param string [String] string to escape
|
128
136
|
# @return [String] escaped string
|
129
137
|
def safe_escape(string)
|
130
|
-
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
|
131
|
-
'%' <<
|
138
|
+
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do |match|
|
139
|
+
'%' << match.unpack('H2' * match.bytesize).join('%').upcase
|
132
140
|
end
|
133
141
|
end
|
134
142
|
|
@@ -168,7 +176,8 @@ module Miasma
|
|
168
176
|
# @return [String] signature
|
169
177
|
def generate(http_method, path, opts)
|
170
178
|
signature = generate_signature(http_method, path, opts)
|
171
|
-
"#{algorithm} Credential=#{access_key}/#{credential_scope},
|
179
|
+
"#{algorithm} Credential=#{access_key}/#{credential_scope}, " \
|
180
|
+
"SignedHeaders=#{signed_headers(opts[:headers])}, Signature=#{signature}"
|
172
181
|
end
|
173
182
|
|
174
183
|
# Generate URL with signed params
|
@@ -185,7 +194,10 @@ module Miasma
|
|
185
194
|
'X-Amz-Credential' => "#{access_key}/#{credential_scope}"
|
186
195
|
)
|
187
196
|
)
|
188
|
-
signature = generate_signature(
|
197
|
+
signature = generate_signature(
|
198
|
+
http_method, path,
|
199
|
+
opts.merge(:body => 'UNSIGNED-PAYLOAD')
|
200
|
+
)
|
189
201
|
params = opts[:params].merge('X-Amz-Signature' => signature)
|
190
202
|
"https://#{opts[:headers]['Host']}/#{path}?#{canonical_query(params)}"
|
191
203
|
end
|
@@ -326,6 +338,7 @@ module Miasma
|
|
326
338
|
|
327
339
|
end
|
328
340
|
|
341
|
+
# Common API setup
|
329
342
|
module ApiCommon
|
330
343
|
|
331
344
|
def self.included(klass)
|
@@ -340,17 +353,22 @@ module Miasma
|
|
340
353
|
attribute :aws_sts_session_token, String
|
341
354
|
attribute :aws_sts_session_token_code, [String, Proc, Method]
|
342
355
|
attribute :aws_sts_mfa_serial_number, [String]
|
343
|
-
attribute :aws_credentials_file, String, :required => true,
|
344
|
-
|
356
|
+
attribute :aws_credentials_file, String, :required => true,
|
357
|
+
:default => File.join(Dir.home, '.aws/credentials')
|
358
|
+
attribute :aws_config_file, String, :required => true,
|
359
|
+
:default => File.join(Dir.home, '.aws/config')
|
345
360
|
attribute :aws_access_key_id, String, :required => true
|
346
361
|
attribute :aws_secret_access_key, String, :required => true
|
347
362
|
attribute :aws_iam_instance_profile, [TrueClass, FalseClass], :default => false
|
363
|
+
attribute :aws_ecs_task_profile, [TrueClass, FalseClass], :default => false
|
348
364
|
attribute :aws_region, String, :required => true
|
349
365
|
attribute :aws_host, String
|
350
366
|
attribute :aws_bucket_region, String
|
351
367
|
attribute :api_endpoint, String, :required => true, :default => 'amazonaws.com'
|
352
|
-
attribute :euca_compat, Symbol, :allowed_values => [:path, :dns],
|
353
|
-
|
368
|
+
attribute :euca_compat, Symbol, :allowed_values => [:path, :dns],
|
369
|
+
:coerce => lambda{|v| v.is_a?(String) ? v.to_sym : v}
|
370
|
+
attribute :euca_dns_map, Smash, :coerce => lambda{|v| v.to_smash},
|
371
|
+
:default => Smash.new
|
354
372
|
attribute :ssl_enabled, [TrueClass, FalseClass], :default => true
|
355
373
|
end
|
356
374
|
|
@@ -361,11 +379,21 @@ module Miasma
|
|
361
379
|
'role_arn' => 'aws_sts_role_arn',
|
362
380
|
'aws_security_token' => 'aws_sts_token',
|
363
381
|
'aws_session_token' => 'aws_sts_session_token'
|
364
|
-
)
|
382
|
+
).to_smash.freeze
|
383
|
+
)
|
384
|
+
klass.const_set(:INSTANCE_PROFILE_HOST, 'http://169.254.169.254'.freeze)
|
385
|
+
klass.const_set(
|
386
|
+
:INSTANCE_PROFILE_PATH,
|
387
|
+
'latest/meta-data/iam/security-credentials'.freeze
|
388
|
+
)
|
389
|
+
klass.const_set(
|
390
|
+
:INSTANCE_PROFILE_AZ_PATH,
|
391
|
+
'latest/meta-data/placement/availability-zone'.freeze
|
392
|
+
)
|
393
|
+
klass.const_set(:ECS_TASK_PROFILE_HOST, 'http://169.254.170.2'.freeze)
|
394
|
+
klass.const_set(
|
395
|
+
:ECS_TASK_PROFILE_PATH, ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']
|
365
396
|
)
|
366
|
-
klass.const_set(:INSTANCE_PROFILE_HOST, 'http://169.254.169.254')
|
367
|
-
klass.const_set(:INSTANCE_PROFILE_PATH, 'latest/meta-data/iam/security-credentials')
|
368
|
-
klass.const_set(:INSTANCE_PROFILE_AZ_PATH, 'latest/meta-data/placement/availability-zone')
|
369
397
|
end
|
370
398
|
|
371
399
|
# Build new API for specified type using current provider / creds
|
@@ -406,7 +434,9 @@ module Miasma
|
|
406
434
|
)
|
407
435
|
end
|
408
436
|
if(creds[:aws_iam_instance_profile])
|
409
|
-
|
437
|
+
self.class.const_get(:ECS_TASK_PROFILE_PATH).nil? ?
|
438
|
+
load_instance_credentials!(creds) :
|
439
|
+
load_ecs_credentials(creds)
|
410
440
|
end
|
411
441
|
true
|
412
442
|
end
|
@@ -418,7 +448,7 @@ module Miasma
|
|
418
448
|
# @return [TrueClass]
|
419
449
|
def after_setup(creds)
|
420
450
|
skip = self.class.attributes.keys.map(&:to_s)
|
421
|
-
creds.each do |k,v|
|
451
|
+
creds.each do |k, v|
|
422
452
|
k = k.to_s
|
423
453
|
if(k.start_with?('aws_') && !skip.include?(k))
|
424
454
|
data[k] = v
|
@@ -452,33 +482,82 @@ module Miasma
|
|
452
482
|
data = {}
|
453
483
|
end
|
454
484
|
end
|
455
|
-
creds
|
456
|
-
creds[:aws_secret_access_key] = data['SecretAccessKey']
|
457
|
-
creds[:aws_sts_token] = data['Token']
|
458
|
-
creds[:aws_sts_token_expires] = Time.xmlschema(data['Expiration'])
|
485
|
+
creds.merge!(extract_creds(data))
|
459
486
|
unless(creds[:aws_region])
|
460
|
-
|
461
|
-
[
|
462
|
-
self.class.const_get(:INSTANCE_PROFILE_HOST),
|
463
|
-
self.class.const_get(:INSTANCE_PROFILE_AZ_PATH)
|
464
|
-
].join('/')
|
465
|
-
).body.to_s.strip
|
466
|
-
az.sub!(/[a-zA-Z]+$/, '')
|
467
|
-
creds[:aws_region] = az
|
487
|
+
creds[:aws_region] = get_region
|
468
488
|
end
|
469
489
|
true
|
470
490
|
end
|
471
491
|
|
492
|
+
# Attempt to load credentials from instance metadata
|
493
|
+
#
|
494
|
+
# @param creds [Hash]
|
495
|
+
# @return [TrueClass]
|
496
|
+
def load_ecs_credentials!(creds)
|
497
|
+
# As per docs ECS_TASK_PROFILE_PATH is defined as
|
498
|
+
# /credential_provider_version/credentials?id=task_UUID
|
499
|
+
# where AWS fills in the version and UUID.
|
500
|
+
# @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
|
501
|
+
data = HTTP.get(
|
502
|
+
[
|
503
|
+
self.class.const_get(:ECS_TASK_PROFILE_HOST),
|
504
|
+
self.class.const_get(:ECS_TASK_PROFILE_PATH)
|
505
|
+
].join
|
506
|
+
).body
|
507
|
+
unless(data.is_a?(Hash))
|
508
|
+
begin
|
509
|
+
data = MultiJson.load(data.to_s)
|
510
|
+
rescue MultiJson::ParseError
|
511
|
+
data = {}
|
512
|
+
end
|
513
|
+
end
|
514
|
+
creds.merge!(extract_creds(data))
|
515
|
+
unless(creds[:aws_region])
|
516
|
+
creds[:aws_region] = get_region
|
517
|
+
end
|
518
|
+
true
|
519
|
+
end
|
520
|
+
|
521
|
+
# Return hash with needed information to assume role
|
522
|
+
#
|
523
|
+
# @param data [Hash]
|
524
|
+
# @return [Hash]
|
525
|
+
def extract_creds(data)
|
526
|
+
c = Smash.new
|
527
|
+
c[:aws_access_key_id] = data['AccessKeyId']
|
528
|
+
c[:aws_secret_access_key] = data['SecretAccessKey']
|
529
|
+
c[:aws_sts_token] = data['Token']
|
530
|
+
c[:aws_sts_token_expires] = Time.xmlschema(data['Expiration'])
|
531
|
+
c[:aws_sts_role_arn] = data['RoleArn'] # used in ECS Role but not instance role
|
532
|
+
c
|
533
|
+
end
|
534
|
+
|
535
|
+
# Return region from meta-data service
|
536
|
+
#
|
537
|
+
# @return [String]
|
538
|
+
def get_region
|
539
|
+
az = HTTP.get(
|
540
|
+
[
|
541
|
+
self.class.const_get(:INSTANCE_PROFILE_HOST),
|
542
|
+
self.class.const_get(:INSTANCE_PROFILE_AZ_PATH)
|
543
|
+
].join('/')
|
544
|
+
).body.to_s.strip
|
545
|
+
az.sub!(/[a-zA-Z]+$/, '')
|
546
|
+
az
|
547
|
+
end
|
548
|
+
|
472
549
|
def sts_mfa_session!(creds)
|
473
550
|
if(sts_mfa_session_update_required?(creds))
|
474
551
|
sts = Miasma::Contrib::Aws::Api::Sts.new(
|
475
552
|
:aws_access_key_id => creds[:aws_access_key_id],
|
476
553
|
:aws_secret_access_key => creds[:aws_secret_access_key],
|
477
554
|
:aws_region => creds.fetch(:aws_sts_region, 'us-east-1'),
|
478
|
-
:aws_credentials_file => creds.fetch(
|
555
|
+
:aws_credentials_file => creds.fetch(
|
556
|
+
:aws_credentials_file, aws_credentials_file
|
557
|
+
),
|
479
558
|
:aws_config_file => creds.fetch(:aws_config_file, aws_config_file),
|
480
559
|
:aws_profile_name => creds[:aws_profile_name],
|
481
|
-
:aws_host => creds[:aws_sts_host]
|
560
|
+
:aws_host => creds[:aws_sts_host]
|
482
561
|
)
|
483
562
|
creds.merge!(
|
484
563
|
sts.mfa_session(
|
@@ -500,7 +579,9 @@ module Miasma
|
|
500
579
|
:aws_access_key_id => get_credential(:access_key_id, creds),
|
501
580
|
:aws_secret_access_key => get_credential(:secret_access_key, creds),
|
502
581
|
:aws_region => creds.fetch(:aws_sts_region, 'us-east-1'),
|
503
|
-
:aws_credentials_file => creds.fetch(
|
582
|
+
:aws_credentials_file => creds.fetch(
|
583
|
+
:aws_credentials_file, aws_credentials_file
|
584
|
+
),
|
504
585
|
:aws_config_file => creds.fetch(:aws_config_file, aws_config_file),
|
505
586
|
:aws_host => creds[:aws_sts_host],
|
506
587
|
:aws_sts_token => creds[:aws_sts_session_token]
|
@@ -521,7 +602,7 @@ module Miasma
|
|
521
602
|
# @param profile [String] name of profile to load
|
522
603
|
# @return [Smash]
|
523
604
|
def load_aws_file(file_path, profile)
|
524
|
-
if(File.
|
605
|
+
if(File.exist?(file_path))
|
525
606
|
l_config = Smash.new.tap do |creds|
|
526
607
|
key = nil
|
527
608
|
File.readlines(file_path).each_with_index do |line, idx|
|
@@ -529,13 +610,18 @@ module Miasma
|
|
529
610
|
next if line.empty? || line.start_with?('#')
|
530
611
|
if(line.start_with?('['))
|
531
612
|
unless(line.end_with?(']'))
|
532
|
-
raise ArgumentError.new(
|
613
|
+
raise ArgumentError.new(
|
614
|
+
"Failed to parse aws file! (#{file_path} line #{idx + 1})"
|
615
|
+
)
|
533
616
|
end
|
534
617
|
key = line.tr('[]', '').strip.sub(/^profile /, '')
|
535
618
|
creds[key] = Smash.new
|
536
619
|
else
|
537
620
|
unless(key)
|
538
|
-
raise ArgumentError.new(
|
621
|
+
raise ArgumentError.new(
|
622
|
+
"Failed to parse aws file! (#{file_path} line #{idx + 1}) " \
|
623
|
+
'- No section defined!'
|
624
|
+
)
|
539
625
|
end
|
540
626
|
line_args = line.split('=', 2).map(&:strip)
|
541
627
|
line_args.first.replace(
|
@@ -545,14 +631,18 @@ module Miasma
|
|
545
631
|
)
|
546
632
|
if(line_args.last.start_with?('"'))
|
547
633
|
unless(line_args.last.end_with?('"'))
|
548
|
-
raise ArgumentError.new(
|
634
|
+
raise ArgumentError.new(
|
635
|
+
"Failed to parse aws file! (#{file_path} line #{idx + 1})"
|
636
|
+
)
|
549
637
|
end
|
550
638
|
line_args.last.replace(line_args.last[1..-2]) # NOTE: strip quoted values
|
551
639
|
end
|
552
640
|
begin
|
553
641
|
creds[key].merge!(Smash[*line_args])
|
554
642
|
rescue => e
|
555
|
-
raise ArgumentError.new(
|
643
|
+
raise ArgumentError.new(
|
644
|
+
"Failed to parse aws file! (#{file_path} line #{idx + 1})"
|
645
|
+
)
|
556
646
|
end
|
557
647
|
end
|
558
648
|
end
|
@@ -659,7 +749,8 @@ module Miasma
|
|
659
749
|
dest, options = request_args
|
660
750
|
path = URI.parse(dest).path
|
661
751
|
options = options ? options.to_smash : Smash.new
|
662
|
-
options[:headers] = Smash[connection.default_options.headers.to_a].
|
752
|
+
options[:headers] = Smash[connection.default_options.headers.to_a].
|
753
|
+
merge(options.fetch(:headers, Smash.new))
|
663
754
|
if(self.class::API_VERSION)
|
664
755
|
if(options[:form])
|
665
756
|
options.set(:form, 'Version', self.class::API_VERSION)
|
@@ -687,7 +778,7 @@ module Miasma
|
|
687
778
|
end
|
688
779
|
signature = signer.generate(http_method, path, options)
|
689
780
|
update_request(connection, options)
|
690
|
-
options = Hash[options.map{|k,v|[k.to_sym,v]}]
|
781
|
+
options = Hash[options.map{|k, v| [k.to_sym, v] }]
|
691
782
|
connection.auth(signature).send(http_method, dest, options)
|
692
783
|
end
|
693
784
|
|
@@ -705,8 +796,12 @@ module Miasma
|
|
705
796
|
# @return [TrueClass, FalseClass]
|
706
797
|
# @note update check only applied if assuming role
|
707
798
|
def sts_mfa_session_update_required?(args={})
|
708
|
-
if(args.fetch(:aws_sts_session_token_code,
|
709
|
-
|
799
|
+
if(args.fetch(:aws_sts_session_token_code,
|
800
|
+
attributes[:aws_sts_session_token_code]))
|
801
|
+
expiry = args.fetch(
|
802
|
+
:aws_sts_session_token_expires,
|
803
|
+
attributes[:aws_sts_session_token_expires]
|
804
|
+
)
|
710
805
|
expiry.nil? || expiry <= Time.now - 15
|
711
806
|
else
|
712
807
|
false
|
@@ -3,12 +3,13 @@ require 'miasma'
|
|
3
3
|
module Miasma
|
4
4
|
module Models
|
5
5
|
class AutoScale
|
6
|
+
# AWS autoscaling API
|
6
7
|
class Aws < AutoScale
|
7
8
|
|
8
9
|
# Service name of the API
|
9
|
-
API_SERVICE = 'autoscaling'
|
10
|
+
API_SERVICE = 'autoscaling'.freeze
|
10
11
|
# Supported version of the AutoScaling API
|
11
|
-
API_VERSION = '2011-01-01'
|
12
|
+
API_VERSION = '2011-01-01'.freeze
|
12
13
|
|
13
14
|
include Contrib::AwsApiCore::ApiCommon
|
14
15
|
include Contrib::AwsApiCore::RequestUtils
|
@@ -49,7 +50,10 @@ module Miasma
|
|
49
50
|
if(group)
|
50
51
|
params.merge('AutoScalingGroupNames.member.1' => group.id || group.name)
|
51
52
|
end
|
52
|
-
result = all_result_pages(nil, :body,
|
53
|
+
result = all_result_pages(nil, :body,
|
54
|
+
'DescribeAutoScalingGroupsResponse', 'DescribeAutoScalingGroupsResult',
|
55
|
+
'AutoScalingGroups', 'member'
|
56
|
+
) do |options|
|
53
57
|
request(
|
54
58
|
:method => :post,
|
55
59
|
:path => '/',
|
@@ -65,14 +69,15 @@ module Miasma
|
|
65
69
|
:minimum_size => grp['MinSize'],
|
66
70
|
:maximum_size => grp['MaxSize'],
|
67
71
|
:status => grp['Status'],
|
68
|
-
:load_balancers => [
|
72
|
+
:load_balancers => [
|
73
|
+
grp.get('LoadBalancerNames', 'member')
|
74
|
+
].flatten(1).compact.map{|i|
|
69
75
|
Group::Balancer.new(self, :id => i, :name => i)
|
70
76
|
}
|
71
77
|
).valid_state
|
72
78
|
end
|
73
79
|
end
|
74
80
|
|
75
|
-
|
76
81
|
# Return all auto scale groups
|
77
82
|
#
|
78
83
|
# @param options [Hash] filter
|
@@ -8,9 +8,9 @@ module Miasma
|
|
8
8
|
class Aws < Compute
|
9
9
|
|
10
10
|
# Service name of the API
|
11
|
-
API_SERVICE = 'ec2'
|
11
|
+
API_SERVICE = 'ec2'.freeze
|
12
12
|
# Supported version of the EC2 API
|
13
|
-
API_VERSION = '2014-06-15'
|
13
|
+
API_VERSION = '2014-06-15'.freeze
|
14
14
|
|
15
15
|
include Contrib::AwsApiCore::ApiCommon
|
16
16
|
include Contrib::AwsApiCore::RequestUtils
|
@@ -23,7 +23,7 @@ module Miasma
|
|
23
23
|
'terminated' => :terminated,
|
24
24
|
'stopping' => :pending,
|
25
25
|
'stopped' => :stopped
|
26
|
-
)
|
26
|
+
).to_smash(:freeze)
|
27
27
|
|
28
28
|
# @todo catch bad lookup and clear model
|
29
29
|
def server_reload(server)
|
@@ -35,15 +35,24 @@ module Miasma
|
|
35
35
|
'InstanceId.1' => server.id
|
36
36
|
}
|
37
37
|
)
|
38
|
-
srv = result.get(:body,
|
38
|
+
srv = result.get(:body,
|
39
|
+
'DescribeInstancesResponse', 'reservationSet',
|
40
|
+
'item', 'instancesSet', 'item'
|
41
|
+
)
|
39
42
|
server.load_data(
|
40
43
|
:id => srv[:instanceId],
|
41
|
-
:name => [srv.fetch(:tagSet, :item, [])].flatten.map{|tag|
|
44
|
+
:name => [srv.fetch(:tagSet, :item, [])].flatten.map{|tag|
|
45
|
+
tag[:value] if tag.is_a?(Hash) && tag[:key] == 'Name'
|
46
|
+
}.compact.first,
|
42
47
|
:image_id => srv[:imageId],
|
43
48
|
:flavor_id => srv[:instanceType],
|
44
49
|
:state => SERVER_STATE_MAP.fetch(srv.get(:instanceState, :name), :pending),
|
45
|
-
:addresses_private => [
|
46
|
-
|
50
|
+
:addresses_private => [
|
51
|
+
Server::Address.new(:version => 4, :address => srv[:privateIpAddress])
|
52
|
+
],
|
53
|
+
:addresses_public => [
|
54
|
+
Server::Address.new(:version => 4, :address => srv[:ipAddress])
|
55
|
+
],
|
47
56
|
:status => srv.get(:instanceState, :name),
|
48
57
|
:key_name => srv[:keyName]
|
49
58
|
)
|
@@ -80,7 +89,9 @@ module Miasma
|
|
80
89
|
'MaxCount' => 1
|
81
90
|
}
|
82
91
|
)
|
83
|
-
server.id = result.get(:body,
|
92
|
+
server.id = result.get(:body,
|
93
|
+
'RunInstancesResponse', 'instancesSet', 'item', 'instanceId'
|
94
|
+
)
|
84
95
|
server.valid_state
|
85
96
|
request(
|
86
97
|
:method => :post,
|
@@ -99,7 +110,9 @@ module Miasma
|
|
99
110
|
|
100
111
|
# @todo need to add auto pagination helper (as common util)
|
101
112
|
def server_all
|
102
|
-
results = all_result_pages(nil, :body,
|
113
|
+
results = all_result_pages(nil, :body,
|
114
|
+
'DescribeInstancesResponse', 'reservationSet', 'item'
|
115
|
+
) do |options|
|
103
116
|
request(
|
104
117
|
:method => :post,
|
105
118
|
:path => '/',
|
@@ -108,17 +121,23 @@ module Miasma
|
|
108
121
|
)
|
109
122
|
)
|
110
123
|
end
|
111
|
-
results.map do |
|
112
|
-
[
|
124
|
+
results.map do |server|
|
125
|
+
[server[:instancesSet][:item]].flatten.compact.map do |srv|
|
113
126
|
Server.new(
|
114
127
|
self,
|
115
128
|
:id => srv[:instanceId],
|
116
|
-
:name => srv.fetch(:tagSet, :item, []).map{|tag|
|
129
|
+
:name => srv.fetch(:tagSet, :item, []).map{|tag|
|
130
|
+
tag[:value] if tag.is_a?(Hash) && tag[:key] == 'Name'
|
131
|
+
}.compact.first,
|
117
132
|
:image_id => srv[:imageId],
|
118
133
|
:flavor_id => srv[:instanceType],
|
119
134
|
:state => SERVER_STATE_MAP.fetch(srv.get(:instanceState, :name), :pending),
|
120
|
-
:addresses_private => [
|
121
|
-
|
135
|
+
:addresses_private => [
|
136
|
+
Server::Address.new(:version => 4, :address => srv[:privateIpAddress])
|
137
|
+
],
|
138
|
+
:addresses_public => [
|
139
|
+
Server::Address.new(:version => 4, :address => srv[:ipAddress])
|
140
|
+
],
|
122
141
|
:status => srv.get(:instanceState, :name),
|
123
142
|
:key_name => srv[:keyName]
|
124
143
|
).valid_state
|
@@ -129,5 +148,4 @@ module Miasma
|
|
129
148
|
end
|
130
149
|
end
|
131
150
|
end
|
132
|
-
|
133
151
|
end
|
@@ -3,15 +3,16 @@ require 'miasma'
|
|
3
3
|
module Miasma
|
4
4
|
module Models
|
5
5
|
class LoadBalancer
|
6
|
+
# AWS load balancer API
|
6
7
|
class Aws < LoadBalancer
|
7
8
|
|
8
9
|
include Contrib::AwsApiCore::ApiCommon
|
9
10
|
include Contrib::AwsApiCore::RequestUtils
|
10
11
|
|
11
12
|
# Service name of API
|
12
|
-
API_SERVICE = 'elasticloadbalancing'
|
13
|
+
API_SERVICE = 'elasticloadbalancing'.freeze
|
13
14
|
# Supported version of the ELB API
|
14
|
-
API_VERSION = '2012-06-01'
|
15
|
+
API_VERSION = '2012-06-01'.freeze
|
15
16
|
|
16
17
|
# Save load balancer
|
17
18
|
#
|
@@ -23,7 +24,7 @@ module Miasma
|
|
23
24
|
'LoadBalancerName' => balancer.name
|
24
25
|
)
|
25
26
|
availability_zones.each_with_index do |az, i|
|
26
|
-
params["AvailabilityZones.member.#{i+1}"] = az
|
27
|
+
params["AvailabilityZones.member.#{i + 1}"] = az
|
27
28
|
end
|
28
29
|
if(balancer.listeners)
|
29
30
|
balancer.listeners.each_with_index do |listener, i|
|
@@ -47,7 +48,9 @@ module Miasma
|
|
47
48
|
)
|
48
49
|
)
|
49
50
|
balancer.public_addresses = [
|
50
|
-
:address => result.get(:body,
|
51
|
+
:address => result.get(:body,
|
52
|
+
'CreateLoadBalancerResponse', 'CreateLoadBalancerResult', 'DNSName'
|
53
|
+
)
|
51
54
|
]
|
52
55
|
balancer.load_data(:id => balancer.name).valid_state
|
53
56
|
if(balancer.health_check)
|
@@ -114,9 +117,12 @@ module Miasma
|
|
114
117
|
def load_balancer_data(balancer=nil)
|
115
118
|
params = Smash.new('Action' => 'DescribeLoadBalancers')
|
116
119
|
if(balancer)
|
117
|
-
params
|
120
|
+
params['LoadBalancerNames.member.1'] = balancer.id || balancer.name
|
118
121
|
end
|
119
|
-
result = all_result_pages(nil, :body,
|
122
|
+
result = all_result_pages(nil, :body,
|
123
|
+
'DescribeLoadBalancersResponse', 'DescribeLoadBalancersResult',
|
124
|
+
'LoadBalancerDescriptions', 'member'
|
125
|
+
) do |options|
|
120
126
|
request(
|
121
127
|
:method => :post,
|
122
128
|
:path => '/',
|
@@ -124,7 +130,10 @@ module Miasma
|
|
124
130
|
)
|
125
131
|
end
|
126
132
|
if(balancer)
|
127
|
-
health_result = all_result_pages(nil, :body,
|
133
|
+
health_result = all_result_pages(nil, :body,
|
134
|
+
'DescribeInstanceHealthResponse', 'DescribeInstanceHealthResult',
|
135
|
+
'InstanceStates', 'member'
|
136
|
+
) do |options|
|
128
137
|
request(
|
129
138
|
:method => :post,
|
130
139
|
:path => '/',
|
@@ -156,7 +165,13 @@ module Miasma
|
|
156
165
|
).merge(
|
157
166
|
health_result.nil? ? {} : Smash.new(
|
158
167
|
:server_states => health_result.nil? ? nil : health_result.map{|i|
|
159
|
-
Balancer::ServerState.new(
|
168
|
+
Balancer::ServerState.new(
|
169
|
+
self.api_for(:compute),
|
170
|
+
:id => i['InstanceId'],
|
171
|
+
:status => i['State'],
|
172
|
+
:reason => i['ReasonCode'],
|
173
|
+
:state => i['State'] == 'InService' ? :up : :down
|
174
|
+
)
|
160
175
|
}
|
161
176
|
)
|
162
177
|
)
|
@@ -206,7 +221,9 @@ module Miasma
|
|
206
221
|
:form => Smash.new(
|
207
222
|
'Action' => 'DescribeAvailabilityZones'
|
208
223
|
)
|
209
|
-
).fetch(:body,
|
224
|
+
).fetch(:body,
|
225
|
+
'DescribeAvailabilityZonesResponse', 'availabilityZoneInfo', 'item', []
|
226
|
+
)
|
210
227
|
[res].flatten.compact.map do |item|
|
211
228
|
if(item['zoneState'] == 'available')
|
212
229
|
item['zoneName']
|
@@ -3,6 +3,7 @@ require 'miasma'
|
|
3
3
|
module Miasma
|
4
4
|
module Models
|
5
5
|
class Orchestration
|
6
|
+
# AWS Orchestration API
|
6
7
|
class Aws < Orchestration
|
7
8
|
|
8
9
|
# Extended stack model to provide AWS specific stack options
|
@@ -12,20 +13,20 @@ module Miasma
|
|
12
13
|
end
|
13
14
|
|
14
15
|
# Service name of the API
|
15
|
-
API_SERVICE = 'cloudformation'
|
16
|
+
API_SERVICE = 'cloudformation'.freeze
|
16
17
|
# Service name of the eucalyptus API
|
17
|
-
EUCA_API_SERVICE = 'CloudFormation'
|
18
|
+
EUCA_API_SERVICE = 'CloudFormation'.freeze
|
18
19
|
# Supported version of the AutoScaling API
|
19
|
-
API_VERSION = '2010-05-15'
|
20
|
+
API_VERSION = '2010-05-15'.freeze
|
20
21
|
|
21
22
|
# Valid stack lookup states
|
22
23
|
STACK_STATES = [
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
]
|
24
|
+
'CREATE_COMPLETE', 'CREATE_FAILED', 'CREATE_IN_PROGRESS', 'DELETE_FAILED',
|
25
|
+
'DELETE_IN_PROGRESS', 'ROLLBACK_COMPLETE', 'ROLLBACK_FAILED', 'ROLLBACK_IN_PROGRESS',
|
26
|
+
'UPDATE_COMPLETE', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', 'UPDATE_IN_PROGRESS',
|
27
|
+
'UPDATE_ROLLBACK_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'UPDATE_ROLLBACK_FAILED',
|
28
|
+
'UPDATE_ROLLBACK_IN_PROGRESS'
|
29
|
+
].map(&:freeze).freeze
|
29
30
|
|
30
31
|
include Contrib::AwsApiCore::ApiCommon
|
31
32
|
include Contrib::AwsApiCore::RequestUtils
|
@@ -48,7 +49,7 @@ module Miasma
|
|
48
49
|
:api => :orchestration,
|
49
50
|
:collection => :stacks
|
50
51
|
)
|
51
|
-
)
|
52
|
+
).to_smash(:freeze)
|
52
53
|
|
53
54
|
# Fetch stacks or update provided stack data
|
54
55
|
#
|
@@ -62,7 +63,10 @@ module Miasma
|
|
62
63
|
end
|
63
64
|
if(stack)
|
64
65
|
d_params['StackName'] = stack.id
|
65
|
-
descriptions = all_result_pages(nil, :body,
|
66
|
+
descriptions = all_result_pages(nil, :body,
|
67
|
+
'DescribeStacksResponse', 'DescribeStacksResult',
|
68
|
+
'Stacks', 'member'
|
69
|
+
) do |options|
|
66
70
|
request(
|
67
71
|
:method => :post,
|
68
72
|
:path => '/',
|
@@ -70,7 +74,10 @@ module Miasma
|
|
70
74
|
)
|
71
75
|
end
|
72
76
|
else
|
73
|
-
lists = all_result_pages(nil, :body,
|
77
|
+
lists = all_result_pages(nil, :body,
|
78
|
+
'ListStacksResponse', 'ListStacksResult',
|
79
|
+
'StackSummaries', 'member'
|
80
|
+
) do |options|
|
74
81
|
request(
|
75
82
|
:method => :post,
|
76
83
|
:path => '/',
|
@@ -143,9 +150,18 @@ module Miasma
|
|
143
150
|
# @return [Models::Orchestration::Stack]
|
144
151
|
def stack_save(stack)
|
145
152
|
params = Smash.new('StackName' => stack.name)
|
153
|
+
if(stack.dirty?(:parameters))
|
154
|
+
initial_parameters = stack.data[:parameters] || {}
|
155
|
+
else
|
156
|
+
initial_parameters = {}
|
157
|
+
end
|
146
158
|
(stack.parameters || {}).each_with_index do |pair, idx|
|
147
159
|
params["Parameters.member.#{idx + 1}.ParameterKey"] = pair.first
|
148
|
-
|
160
|
+
if(initial_parameters[pair.first] == pair.last)
|
161
|
+
params["Parameters.member.#{idx + 1}.UsePreviousValue"] = true
|
162
|
+
else
|
163
|
+
params["Parameters.member.#{idx + 1}.ParameterValue"] = pair.last
|
164
|
+
end
|
149
165
|
end
|
150
166
|
(stack.capabilities || []).each_with_index do |cap, idx|
|
151
167
|
params["Capabilities.member.#{idx + 1}"] = cap
|
@@ -171,12 +187,10 @@ module Miasma
|
|
171
187
|
end
|
172
188
|
if(stack.template_url)
|
173
189
|
params['TemplateURL'] = stack.template_url
|
190
|
+
elsif(!stack.dirty?(:template) && stack.persisted?)
|
191
|
+
params['UsePreviousTemplate'] = true
|
174
192
|
else
|
175
|
-
|
176
|
-
params['UsePreviousTemplate'] = true
|
177
|
-
else
|
178
|
-
params['TemplateBody'] = MultiJson.dump(stack.template)
|
179
|
-
end
|
193
|
+
params['TemplateBody'] = MultiJson.dump(stack.template)
|
180
194
|
end
|
181
195
|
if(stack.persisted?)
|
182
196
|
result = request(
|
@@ -316,7 +330,10 @@ module Miasma
|
|
316
330
|
# @param stack [Models::Orchestration::Stack]
|
317
331
|
# @return [Array<Models::Orchestration::Stack::Resource>]
|
318
332
|
def resource_all(stack)
|
319
|
-
results = all_result_pages(nil, :body,
|
333
|
+
results = all_result_pages(nil, :body,
|
334
|
+
'ListStackResourcesResponse', 'ListStackResourcesResult',
|
335
|
+
'StackResourceSummaries', 'member'
|
336
|
+
) do |options|
|
320
337
|
request(
|
321
338
|
:method => :post,
|
322
339
|
:path => '/',
|
@@ -353,7 +370,10 @@ module Miasma
|
|
353
370
|
'LogicalResourceId' => resource.logical_id,
|
354
371
|
'StackName' => resource.stack.name
|
355
372
|
)
|
356
|
-
).get(:body,
|
373
|
+
).get(:body,
|
374
|
+
'DescribeStackResourceResponse', 'DescribeStackResourceResult',
|
375
|
+
'StackResourceDetail'
|
376
|
+
)
|
357
377
|
resource.updated = result['LastUpdatedTimestamp']
|
358
378
|
resource.type = result['ResourceType']
|
359
379
|
resource.state = result['ResourceStatus'].downcase.to_sym
|
@@ -368,7 +388,11 @@ module Miasma
|
|
368
388
|
# @param stack [Models::Orchestration::Stack]
|
369
389
|
# @return [Array<Models::Orchestration::Stack::Event>]
|
370
390
|
def event_all(stack, evt_id=nil)
|
371
|
-
|
391
|
+
evt_id = stack.custom[:last_event_token] if evt_id == true
|
392
|
+
results = all_result_pages(evt_id, :body,
|
393
|
+
'DescribeStackEventsResponse', 'DescribeStackEventsResult',
|
394
|
+
'StackEvents', 'member'
|
395
|
+
) do |options|
|
372
396
|
request(
|
373
397
|
:method => :post,
|
374
398
|
:path => '/',
|
@@ -379,6 +403,7 @@ module Miasma
|
|
379
403
|
)
|
380
404
|
end
|
381
405
|
events = results.map do |event|
|
406
|
+
stack.custom[:last_event_token] = event['NextToken'] if event['NextToken']
|
382
407
|
Stack::Event.new(
|
383
408
|
stack,
|
384
409
|
:id => event['EventId'],
|
@@ -393,8 +418,7 @@ module Miasma
|
|
393
418
|
end
|
394
419
|
if(evt_id)
|
395
420
|
idx = events.index{|d| d.id == evt_id}
|
396
|
-
idx ? idx
|
397
|
-
events.slice(0, idx)
|
421
|
+
idx ? events.slice(0, idx) : events
|
398
422
|
else
|
399
423
|
events
|
400
424
|
end
|
@@ -5,14 +5,15 @@ require 'miasma'
|
|
5
5
|
module Miasma
|
6
6
|
module Models
|
7
7
|
class Storage
|
8
|
+
# AWS storage API
|
8
9
|
class Aws < Storage
|
9
10
|
|
10
11
|
# Service name of the API
|
11
|
-
API_SERVICE = 's3'
|
12
|
+
API_SERVICE = 's3'.freeze
|
12
13
|
# Service name of the API in eucalyptus
|
13
|
-
EUCA_API_SERVICE = 'objectstorage'
|
14
|
+
EUCA_API_SERVICE = 'objectstorage'.freeze
|
14
15
|
# Supported version of the Storage API
|
15
|
-
API_VERSION = '2006-03-01'
|
16
|
+
API_VERSION = '2006-03-01'.freeze
|
16
17
|
|
17
18
|
include Contrib::AwsApiCore::ApiCommon
|
18
19
|
include Contrib::AwsApiCore::RequestUtils
|
@@ -39,7 +40,9 @@ module Miasma
|
|
39
40
|
end
|
40
41
|
set = result.get(*result_key.slice(0, 2))
|
41
42
|
if(set.is_a?(Hash) && set['IsTruncated'] && set['Contents'])
|
42
|
-
content_key = (
|
43
|
+
content_key = (
|
44
|
+
set['Contents'].respond_to?(:last) ? set['Contents'].last : set['Contents']
|
45
|
+
)['Key']
|
43
46
|
list += all_result_pages(content_key, *result_key, &block)
|
44
47
|
end
|
45
48
|
list.compact
|
@@ -256,7 +259,8 @@ module Miasma
|
|
256
259
|
unless(headers.empty?)
|
257
260
|
args[:headers] = headers
|
258
261
|
end
|
259
|
-
if(file.attributes[:body].respond_to?(:read) &&
|
262
|
+
if(file.attributes[:body].respond_to?(:read) &&
|
263
|
+
file.attributes[:body].size >= Storage::MAX_BODY_SIZE_FOR_STRINGIFY)
|
260
264
|
upload_id = request(
|
261
265
|
args.merge(
|
262
266
|
Smash.new(
|
data/miasma-aws.gemspec
CHANGED
@@ -11,12 +11,12 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.license = 'Apache 2.0'
|
12
12
|
s.require_path = 'lib'
|
13
13
|
s.add_runtime_dependency 'miasma', '>= 0.3.1', '< 0.5'
|
14
|
-
s.add_development_dependency 'rake'
|
15
|
-
s.add_development_dependency 'rubocop'
|
14
|
+
s.add_development_dependency 'rake', '~> 10.5.0'
|
15
|
+
s.add_development_dependency 'rubocop', '0.37.2'
|
16
16
|
s.add_development_dependency 'pry'
|
17
17
|
s.add_development_dependency 'vcr'
|
18
18
|
s.add_development_dependency 'mocha'
|
19
|
-
s.add_development_dependency 'webmock'
|
19
|
+
s.add_development_dependency 'webmock', '~> 1.23.0'
|
20
20
|
s.add_development_dependency 'minitest'
|
21
21
|
s.add_development_dependency 'minitest-vcr'
|
22
22
|
s.files = Dir['lib/**/*'] + %w(miasma-aws.gemspec README.md CHANGELOG.md LICENSE)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: miasma-aws
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Roberts
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: miasma
|
@@ -34,30 +34,30 @@ dependencies:
|
|
34
34
|
name: rake
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 10.5.0
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- - "
|
44
|
+
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: 10.5.0
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rubocop
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- -
|
51
|
+
- - '='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
53
|
+
version: 0.37.2
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
|
-
- -
|
58
|
+
- - '='
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
60
|
+
version: 0.37.2
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: pry
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,16 +104,16 @@ dependencies:
|
|
104
104
|
name: webmock
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
|
-
- - "
|
107
|
+
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
109
|
+
version: 1.23.0
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
|
-
- - "
|
114
|
+
- - "~>"
|
115
115
|
- !ruby/object:Gem::Version
|
116
|
-
version:
|
116
|
+
version: 1.23.0
|
117
117
|
- !ruby/object:Gem::Dependency
|
118
118
|
name: minitest
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,9 +183,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
183
183
|
version: '0'
|
184
184
|
requirements: []
|
185
185
|
rubyforge_project:
|
186
|
-
rubygems_version: 2.
|
186
|
+
rubygems_version: 2.5.1
|
187
187
|
signing_key:
|
188
188
|
specification_version: 4
|
189
189
|
summary: Smoggy AWS API
|
190
190
|
test_files: []
|
191
|
-
has_rdoc:
|