cloudhealth-setup 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ unless $:.include?(File.dirname(__FILE__) + '/../lib/')
4
+ $: << File.dirname(__FILE__) + '/../lib'
5
+ end
6
+
7
+ require 'rubygems'
8
+ require 'cloudhealth-setup'
9
+
10
+ Setup.run
data/bin/fog ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'fog' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('fog', 'fog')
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'nokogiri' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('nokogiri', 'nokogiri')
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'cloudhealth-setup'
3
+ s.version = '0.0.10'
4
+ s.date = '2013-08-31'
5
+ s.platform = Gem::Platform::RUBY
6
+ s.authors = ['CloudHealth Technologies']
7
+ s.email = ['support@cloudhealthtech.com']
8
+ s.homepage = 'http://www.cloudhealthtech.com'
9
+ s.summary = 'Configures an Amazon AWS account for use with the CloudHealth service'
10
+ s.description = 'Configures an Amazon AWS account for use with the CloudHealth service, including creating a least privilege read only AWS user and enabling the retrieval of cost and usage information.'
11
+ s.license = 'MIT'
12
+ s.has_rdoc = false
13
+
14
+ s.add_dependency("nokogiri", "= 1.5.8")
15
+ s.add_dependency("fog", "= 1.12.1")
16
+ s.add_dependency("json", "= 1.8.0")
17
+ s.add_dependency("excon", "= 0.23.0")
18
+ s.add_dependency("mixlib-cli", "= 1.3.0")
19
+ s.add_dependency("mechanize", "= 2.5.1")
20
+ s.add_dependency("highline", "= 1.6.19")
21
+
22
+ s.bindir = "bin"
23
+ s.files = Dir.glob('{bin,lib}/**/*') + %w[cloudhealth-setup.gemspec]
24
+ s.executables = Dir.glob('bin/**/*').map { |file| File.basename(file) }
25
+ s.require_paths = ['lib']
26
+ end
@@ -0,0 +1,273 @@
1
+ require "highline/system_extensions"
2
+ include HighLine::SystemExtensions
3
+
4
+ class Setup
5
+ def iam
6
+ begin
7
+ Fog::AWS::IAM.new({:aws_access_key_id => @aws_key, :aws_secret_access_key => @aws_secret})
8
+ rescue => e
9
+ critical_failure("Failed to connect to Amazon AWS IAM: Please ensure your provided credentials are correct and you have internet access. #{e if @verbose}")
10
+ end
11
+ end
12
+
13
+ def s3
14
+ begin
15
+ Fog::Storage.new({:provider => 'AWS', :aws_access_key_id => @aws_key, :aws_secret_access_key => @aws_secret})
16
+ rescue => e
17
+ critical_failure("Failed to connect to Amazon AWS S3: Please ensure your provided credentials are correct and you have internet access. #{e if @verbose}")
18
+ end
19
+ end
20
+
21
+ def check_iam_credentials
22
+ puts "Please wait, ensuring the provided credentials work."
23
+ begin
24
+ @iam.list_users
25
+ rescue => e
26
+ if @input_file.nil? || @input_file.empty?
27
+ # Single account mode
28
+ puts "Could not login to your account with the provided Amazon Web Services credentials. Please check your AWS Access Key and Secret Access Key and try again. If issue persists contact support@cloudhealthtech.com."
29
+ exit
30
+ else
31
+ # Multi account mode
32
+ raise SetupFailed, "Could not login to your account with the provided Amazon Web Services credentials. Please check your AWS Access Key and Secret Access Key and try again. If issue persists contact support@cloudhealthtech.com."
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_ro_user
38
+ begin
39
+ if user_exists
40
+ puts "[X] AWS Read-Only user (#{@aws_ro_name}) is setup -- Exists"
41
+ else
42
+ puts "[ ] AWS Read-Only user (#{@aws_ro_name}) not setup -- No user"
43
+ end
44
+ if user_has_policy
45
+ puts "[X] AWS Read-Only user has a policy -- Exists"
46
+ else
47
+ puts "[ ] AWS Read-Only user has no policy -- No policy"
48
+ end
49
+ rescue => e
50
+ puts " We were unable to test your read only user."
51
+ puts " Please contact CloudHealth support at support@cloudhealthtech.com."
52
+ warning(e)
53
+ end
54
+ end
55
+
56
+ def uninstall_ro_user
57
+ begin
58
+ print "Delete Cloudhealth Read-only user #{@aws_ro_name},attached policies, profiles (y/n)? "
59
+ k = get_character
60
+ if k.chr == "y"
61
+ puts "Deleting user #{@aws_ro_name} and all associated policies and login profiles..."
62
+ user_policies = @iam.list_user_policies(@aws_ro_name)
63
+
64
+ user_policies.body['PolicyNames'].each do |policy|
65
+ puts " Deleting user policy #{policy} attached to this user."
66
+ @iam.delete_user_policy(@aws_ro_name, policy)
67
+ end
68
+
69
+ puts " Deleting users access keys..."
70
+ access_keys = @iam.list_access_keys('UserName' => @aws_ro_name).body
71
+
72
+ access_keys['AccessKeys'].each do |access_key|
73
+ puts " Deleting access key #{access_key['AccessKeyId']}..."
74
+ @iam.delete_access_key(access_key['AccessKeyId'], 'UserName' => @aws_ro_name)
75
+ end
76
+
77
+ begin
78
+ puts " Deleting login profile..."
79
+ @iam.delete_login_profile(@aws_ro_name)
80
+ rescue => e
81
+ puts " User does not have a login profile, skipping."
82
+ warning(e)
83
+ end
84
+
85
+ puts " Deleting user...."
86
+ @iam.delete_user(@aws_ro_name)
87
+ puts " IAM User #{@aws_ro_name} deleted."
88
+ else
89
+ puts " You did not agree to delete the AWS Read only user #{aws_ro_name}."
90
+ end
91
+ rescue => e
92
+ if defined? e.response
93
+ if e.response.status == 409
94
+ puts " Could not delete user and/or login profile/policy, subordinate entities exist."
95
+ elsif e.response.status == 404
96
+ puts " Could not delete user and/or login profile/policy, user/profile/policy does not exist."
97
+ else
98
+ puts " Could not delete User or Policy/profile, unknown error: #{e.response.inspect}"
99
+ end
100
+ else
101
+ puts " Could not delete: #{e.message}"
102
+ warning(e) if @verbose
103
+ end
104
+ end
105
+ end
106
+
107
+ def setup_ro_user
108
+ user_created = nil
109
+ begin
110
+ puts "Setting up AWS Read only user... "
111
+ if @aws_ro_name.nil?
112
+ puts " Name not specified -- Skipping."
113
+ else
114
+ if user_exists
115
+ if @ro_user_exists
116
+ puts " User #{@aws_ro_name} exists... continuing due to --user-exists"
117
+ puts " Note: CSV Output file will NOT be complete. Since we did not create the aws read only user"
118
+ puts " You must fill in the blanks of the CSV if you plan on importing the CSV on the website."
119
+ else
120
+ #TODO This should not exit() in multi-account import mode, it should be raised up and caught. e.g. SetupFailed
121
+ puts "User #{@aws_ro_name} exists already, If this was your intention please re-run with --user-exists and ensure you update the CSV manually or choose another username via -r <name>"
122
+ exit
123
+ end
124
+ else
125
+ puts " Creating user... "
126
+ user_create = @iam.create_user(@aws_ro_name)
127
+ key_create = @iam.create_access_key('UserName' => @aws_ro_name)
128
+ access_key = key_create.body['AccessKey']['AccessKeyId']
129
+ secret_key = key_create.body['AccessKey']['SecretAccessKey']
130
+ arn = user_create.body['User']['Arn']
131
+ user_pass = create_user_password
132
+ @created_account.merge!(:access_key => access_key, :secret_key => secret_key, :user_pass => user_pass, :user => @aws_ro_name)
133
+ user_created = " The user #{@aws_ro_name} has been created with password #{user_pass}, access key #{access_key}, and secret key #{secret_key}"
134
+ end
135
+ if user_has_policy
136
+ puts " User policy already exists..."
137
+ else
138
+ puts " Creating user policy... "
139
+ @iam.put_user_policy(@aws_ro_name, "CHTRoPolicy", aws_ro_policy)
140
+ end
141
+ puts user_created unless user_created.nil?
142
+ puts " Setup of AWS Read only user completed"
143
+
144
+ end
145
+ rescue => e
146
+ puts " We were unable to create a read only user."
147
+ puts " Please contact CloudHealth support at support@cloudhealthtech.com."
148
+ warning(e)
149
+ end
150
+ end
151
+
152
+ def test_s3_bucket
153
+ begin
154
+ if @s3.directories.collect{|d| d.key}.include?(@setup_bucket)
155
+ puts "[X] S3 Billing bucket (#{@setup_bucket}) -- Enabled"
156
+ else
157
+ puts "[ ] S3 Billing bucket (#{@setup_bucket}) -- Does not exist"
158
+ end
159
+
160
+ if bucket_has_policy
161
+ puts "[X] S3 Billing bucket policy -- Exists"
162
+ else
163
+ puts "[ ] S3 Billing bucket policy -- No Policy"
164
+ end
165
+ rescue => e
166
+ puts " We were unable to test your S3 billing bucket. You can manually enable this,"
167
+ puts " by following these instructions: http://docs.aws.amazon.com/awsaccountbilling/latest/about/programaccess.html"
168
+ warning(e)
169
+ end
170
+ end
171
+
172
+ def setup_s3_bucket
173
+ begin
174
+ puts "Setting up S3 billing bucket... "
175
+ if @s3.directories.collect{|d| d.key}.include?(@setup_bucket)
176
+ puts " Bucket exists..."
177
+ else
178
+ puts " Creating bucket... "
179
+ begin
180
+ @s3.directories.create(:key => @setup_bucket, :public => false)
181
+ rescue => e
182
+ if e.response.status == 409
183
+ puts " The bucket you are trying to use is already created, but owned by another account. Please use another bucket name."
184
+ else
185
+ raise e
186
+ end
187
+ end
188
+ end
189
+
190
+ if bucket_has_policy
191
+ puts " Bucket already has policy... "
192
+ else
193
+ puts " Creating bucket policy..."
194
+ @s3.put_bucket_policy(@setup_bucket, bucket_policy)
195
+ end
196
+ @created_account.merge!(:s3_bucket => @setup_bucket)
197
+ puts " Bucket setup finished"
198
+ rescue => e
199
+ puts " We were unable to setup an S3 billing bucket. You can manually enable this,"
200
+ puts " by following these instructions: http://docs.aws.amazon.com/awsaccountbilling/latest/about/programaccess.html"
201
+ warning(e)
202
+ end
203
+ end
204
+
205
+ def user_exists
206
+ begin
207
+ @iam.get_user(@aws_ro_name)
208
+ true
209
+ rescue
210
+ false
211
+ end
212
+ end
213
+
214
+ def test_account_alias
215
+ begin
216
+ acct_aliases = @iam.list_account_aliases.body['AccountAliases']
217
+ if acct_aliases.empty?
218
+ puts "[ ] AWS Account alias is not setup, Account ID: #{@aws_account_id} used instead -- Not setup"
219
+ else
220
+ puts "[X] AWS Account alias(es) are setup (#{acct_aliases.map{|aa| aa.strip}.join(', ')}), ID: #{@aws_account_id} -- Setup"
221
+ end
222
+ rescue => e
223
+ puts " We were unable to check for an account alias."
224
+ puts " Please contact CloudHealth support at support@cloudhealthtech.com"
225
+ warning(e)
226
+ end
227
+ end
228
+
229
+ def setup_account_alias
230
+ begin
231
+ if @aws_acct_alias.nil?
232
+ puts "AWS account alias not given, will use account id: #{@created_account[:account_id]} -- Skipping alias setup."
233
+ @created_account.merge!(:account_alias => @created_account[:account_id],
234
+ :account_url => "https://#{@created_account[:account_id]}.signin.aws.amazon.com/")
235
+ else
236
+ puts "Setting up account alias... "
237
+ begin
238
+ @iam.create_account_alias(@aws_acct_alias)
239
+ puts " alias set to #{@aws_acct_alias}..."
240
+ rescue => e
241
+ if e.class == Fog::AWS::IAM::EntityAlreadyExists
242
+ puts " Account alias was already set."
243
+ elsif e.response.status == 409
244
+ puts " Account alias was already set."
245
+ else
246
+ raise e
247
+ end
248
+ end
249
+ @created_account.merge!(:account_alias => @aws_acct_alias,
250
+ :account_url => "https://#{@aws_acct_alias}.signin.aws.amazon.com/")
251
+ end
252
+ rescue => e
253
+ puts " We were unable to create an account alias."
254
+ puts " Please contact CloudHealth support at support@cloudhealthtech.com"
255
+ warning(e)
256
+ end
257
+ end
258
+
259
+ def create_user_password
260
+ puts " Setting user password"
261
+ pw = SecureRandom.hex
262
+ begin
263
+ @iam.create_login_profile(@aws_ro_name, pw)
264
+ rescue => e
265
+ if e.response.status == 409
266
+ puts " User already had a password set..."
267
+ else
268
+ raise e
269
+ end
270
+ end
271
+ pw
272
+ end
273
+ end
@@ -0,0 +1,16 @@
1
+ class SetupFailed < StandardError
2
+ end
3
+
4
+ class Setup
5
+ def critical_failure(message)
6
+ puts message
7
+ #raise SetupFailed, message
8
+ end
9
+
10
+ def warning(e)
11
+ if @verbose
12
+ puts e
13
+ puts e.backtrace
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,336 @@
1
+ class Setup
2
+ AWS_LOGIN_URL = 'https://www.amazon.com/ap/signin?openid.assoc_handle=aws&openid.return_to=https%3A%2F%2Fportal.aws.amazon.com%2Fgp%2Faws%2Fdeveloper%2Faccount%2Findex.html%3Fie%3DUTF8%26action%3Dactivity-summary&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&action=&disableCorpSignUp=&clientContext=&marketPlaceId=&poolName=&authCookies=&pageId=aws.ssop&siteState=&accountStatusPolicy=&sso=&openid.pape.preferred_auth_policies=MultifactorPhysical&openid.pape.max_auth_age=3600&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&server=%2Fap%2Fsignin%3Fie%3DUTF8&accountPoolAlias='
3
+
4
+ def mech_browser
5
+ # Creating Mechanize object
6
+ browser = Mechanize.new
7
+ browser.verify_mode = OpenSSL::SSL::VERIFY_NONE
8
+ browser.redirect_ok = true
9
+ # Let's give some header here
10
+ browser.request_headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4'
11
+ browser.request_headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
12
+ browser.request_headers['application/xml'] = '*/*'
13
+ browser
14
+ end
15
+
16
+ def get_page
17
+ begin
18
+ url = AWS_LOGIN_URL
19
+ page = @browser.get(url)
20
+ signed_in = page.link_with(:text => "Sign Out")
21
+
22
+ if signed_in.nil? # Since @browser is shared, sometimes we are already logged in.
23
+ form = page.form_with(:name => 'signIn')
24
+ form['email'] = @aws_user
25
+ form['password'] = @aws_pass
26
+ login_page = form.submit
27
+
28
+ if login_page.code.to_i != 200
29
+ puts "The login page returned error code #{login_page.code}"
30
+ raise SetupFailed, " Could not login to AWS Web Console, Amazon may be experiencing issues or the credentials you provided are incorrect. HTTP Code: #{login_page.code}"
31
+ end
32
+ end
33
+
34
+ account_id_search = page.search('//span[@class="txtxxsm"]/text()')
35
+ unless account_id_search.nil? || account_id_search.size == 0 || @created_account[:account_id]
36
+ firstw, secondw, account_id_long = account_id_search.first.content.strip.split(" ")
37
+ account_id = account_id_long.gsub("-","")
38
+ puts " Account ID for this account is: #{account_id.to_s}" if @verbose
39
+ @aws_account_id = account_id
40
+ @created_account.merge!(:account_id => account_id)
41
+ end
42
+
43
+ page = @browser.get("https://portal.aws.amazon.com/gp/aws/developer/account?ie=UTF8&action=billing-preferences")
44
+
45
+ if page.code.to_i != 200
46
+ raise SetupFailed, " Could not login to AWS Web Console, Amazon may be experiencing issues or the credentials you provided are incorrect. HTTP Code: #{page.code}"
47
+ end
48
+
49
+ page
50
+ rescue => e
51
+ raise SetupFailed, e
52
+ end
53
+ end
54
+
55
+ def check_web_credentials
56
+ get_page
57
+ get_page # We run this twice, only on the second login do we find our account id on the initial page. This primes the pump so to speak anyway for subsequent usage. TODO: Fix me
58
+ if @aws_account_id.nil? || @aws_account_id.empty?
59
+ if @input_file.nil? || @input_file.empty?
60
+ # Single account mode
61
+ puts "Could not login to your account with the provided Amazon Web Services credentials. Please check your provided email address and password and try again. If issue persists contact support@cloudhealthtech.com."
62
+ exit
63
+ else
64
+ # Multi account mode
65
+ raise SetupFailed, "Could not login to your account with the provided Amazon Web Services credentials. Please check your provided email address and password and try again. If issue persists contact support@cloudhealthtech.com."
66
+ end
67
+ end
68
+ end
69
+
70
+ def test_consolidated
71
+ page = @browser.get("https://portal.aws.amazon.com/gp/aws/developer/account?ie=UTF8&action=consolidated-billing")
72
+ consolidated_link = page.link_with(:href => "https://portal.aws.amazon.com/gp/aws/developer/subscription/index.html?ie=UTF8&productCode=AWSCBill")
73
+ if consolidated_link.nil?
74
+ puts "[O] Account is setup on consolidated billing - Optional"
75
+ else
76
+ #We must be on consolidated billing
77
+ puts "[O] Account is not on consolidated billing - Optional"
78
+ end
79
+ end
80
+
81
+ def account_consolidated
82
+ page = @browser.get("https://portal.aws.amazon.com/gp/aws/developer/account?ie=UTF8&action=consolidated-billing")
83
+ consolidated_link = page.link_with(:href => "https://portal.aws.amazon.com/gp/aws/developer/subscription/index.html?ie=UTF8&productCode=AWSCBill")
84
+ if consolidated_link.nil?
85
+ @created_account.merge!(:consolidated => true)
86
+ puts "Account is on consolidated billing"
87
+ else
88
+ #We must be on consolidated billing
89
+ @created_account.merge!(:consolidated => false)
90
+ puts "Account is not on consolidated billing"
91
+ end
92
+ end
93
+
94
+ def dump_page(page)
95
+ no = rand(500)
96
+ puts "Writing out page as #{no}.html"
97
+ File.open("#{no}.html", 'w') do |file|
98
+ file << page.body
99
+ end
100
+ end
101
+
102
+ def test_monthly_report
103
+ begin
104
+ page = get_page
105
+ monthly_report_form = page.form_with(:name => "csvReportOptInForm")
106
+ mrf_enabled = monthly_report_form.field_with(:name => "buttonOption")
107
+
108
+ if mrf_enabled.value == "EnableCSVReport"
109
+ puts "[ ] Monthly report -- Disabled"
110
+ elsif mrf_enabled.value == "CancelCSVReport"
111
+ puts "[X] Monthly report -- Enabled"
112
+ end
113
+ rescue => e
114
+ puts " We were unable to test if monthly report access is enabled."
115
+ puts " This setting can be checked manually under Billing Preferences from the AWS Account page."
116
+ warning(e)
117
+ end
118
+ end
119
+
120
+ def setup_monthly_report
121
+ begin
122
+ puts "Setting up monthly report access... "
123
+ page = get_page
124
+ monthly_report_form = page.form_with(:name => "csvReportOptInForm")
125
+ mrf_enabled = monthly_report_form.field_with(:name => "buttonOption")
126
+
127
+ if mrf_enabled.value == "EnableCSVReport"
128
+ puts " Report not enabled, enabling... "
129
+ monthly_report_form.submit
130
+ elsif mrf_enabled.value == "CancelCSVReport"
131
+ puts " Report already enabled... "
132
+ end
133
+ puts " Monthly report access setup complete."
134
+ rescue => e
135
+ puts " We were unable to setup monthly report access."
136
+ puts " This setting can be enabled manually under Billing Preferences from the AWS Account page."
137
+ warning(e)
138
+ end
139
+ end
140
+
141
+ def test_prog_access
142
+ begin
143
+ page = get_page
144
+ prog_access_form = page.form_with(:name => "paOptInForm")
145
+ paf_enabled = prog_access_form.field_with(:name => "existingS3BucketName")
146
+
147
+ if paf_enabled.nil?
148
+ puts "[ ] Programmatic access not setup -- Disabled"
149
+ else
150
+ puts "[X] Programmatic access is setup -- Enabled"
151
+ end
152
+ rescue => e
153
+ puts " We were unable to test programmatic access to your billing information."
154
+ puts " You can manually enable/test this by following these instructions: http://docs.aws.amazon.com/awsaccountbilling/latest/about/programaccess.html"
155
+ warning(e)
156
+ end
157
+ end
158
+
159
+ def setup_prog_access
160
+ begin
161
+ puts "Setting up programmatic access to billing... "
162
+ page = get_page
163
+ prog_access_form = page.form_with(:name => "paOptInForm")
164
+ paf_enabled = prog_access_form.field_with(:name => "existingS3BucketName")
165
+
166
+ if paf_enabled.nil?
167
+ puts " Enabling access in bucket #{@setup_bucket}... "
168
+ prog_access_form['s3BucketName'] = @setup_bucket
169
+ prog_access_form.submit
170
+ else
171
+ puts " Access already enabled..."
172
+ end
173
+ puts " Setup of programmatic access to billing finished"
174
+ rescue => e
175
+ puts " We were unable to setup programmatic access to your billing information."
176
+ puts " You can manually enable this by following these instructions: http://docs.aws.amazon.com/awsaccountbilling/latest/about/programaccess.html"
177
+ warning(e)
178
+ end
179
+ end
180
+
181
+ def test_detailed_billing
182
+ begin
183
+ page = get_page
184
+ detailed_billing_form = page.form_with(:name=>'hourlyOptInForm')
185
+ bill_enabled = detailed_billing_form.field_with(:name => "buttonOptionHourly")
186
+
187
+ if bill_enabled.value == "EnableHourly"
188
+ puts "[ ] Detailed billing report not setup -- Disabled"
189
+ elsif bill_enabled.value == "DisableHourly"
190
+ puts "[X] Detailed billing report setup -- Enabled"
191
+ end
192
+ rescue => e
193
+ puts " We were unable to test detailed billing reports."
194
+ puts " This setting can be enabled/tested manually under Billing Preferences from the AWS account page."
195
+ warning(e)
196
+ end
197
+ end
198
+
199
+ def setup_detailed_billing
200
+ begin
201
+ puts "Setting up detailed billing report..."
202
+ page = get_page
203
+ detailed_billing_form = page.form_with(:name=>'hourlyOptInForm')
204
+ bill_enabled = detailed_billing_form.field_with(:name => "buttonOptionHourly")
205
+
206
+ if bill_enabled.value == "EnableHourly"
207
+ puts " Enabling detailed billing report... "
208
+ detailed_billing_form.submit
209
+ elsif bill_enabled.value == "DisableHourly"
210
+ puts " Report already enabled... "
211
+ end
212
+ puts " Report setup finished"
213
+ rescue => e
214
+ puts " We were unable to enable detailed billing reports."
215
+ puts " This setting can be enabled manually under Billing Preferences from the AWS account page."
216
+ warning(e)
217
+ end
218
+ end
219
+
220
+ def test_cost_alloc
221
+ begin
222
+ page = get_page
223
+ cost_alloc_form = page.form_with(:name=>'carOptInForm')
224
+ car_enabled = cost_alloc_form.field_with(:name => "buttonOptionCAR")
225
+
226
+ if car_enabled.value == "EnableCAR"
227
+ puts "[ ] Cost allocation report not setup -- Disabled"
228
+ elsif car_enabled.value == "DisableCAR"
229
+ puts "[X] Cost allocation report setup -- Enabled"
230
+ end
231
+ rescue => e
232
+ puts " We were unable to test cost allocation reports."
233
+ puts " This setting can be tested manually under Billing Preferences from the AWS account page."
234
+ warning(e)
235
+ end
236
+ end
237
+
238
+ def setup_cost_alloc
239
+ begin
240
+ puts "Setting up cost allocation report... "
241
+ page = get_page
242
+ cost_alloc_form = page.form_with(:name=>'carOptInForm')
243
+ car_enabled = cost_alloc_form.field_with(:name => "buttonOptionCAR")
244
+
245
+ if car_enabled.value == "EnableCAR"
246
+ puts " Enabling cost allocation report... "
247
+ cost_alloc_form.submit
248
+ elsif car_enabled.value == "DisableCAR"
249
+ puts " Report already enabled... "
250
+ end
251
+ puts " Report setup finished"
252
+ rescue => e
253
+ puts " We were unable to enable cost allocation reports."
254
+ puts " This setting can be enabled manually under Billing Preferences from the AWS account page."
255
+ warning(e)
256
+ end
257
+ end
258
+
259
+ def test_checkboxes
260
+ begin
261
+ get_page
262
+ page = @browser.get("https://portal.aws.amazon.com/gp/aws/manageYourAccount")
263
+ checkbox_account_activity_search = page.search('[@id="account_activity_checkbox"]')
264
+ usage_report_search = page.search('[@id="usage_reports_checkbox"]')
265
+ activate_button_search = page.search('[@id="activateIAMUserAccess"]')
266
+ deactivate_button_search = page.search('[@id="deactivateIAMUserAccess"]')
267
+
268
+ activate_hidden = begin
269
+ activate_button_search.first.attributes['style'].value.include?("display:none")
270
+ rescue
271
+ false
272
+ end
273
+
274
+ deactivate_hidden = begin
275
+ deactivate_button_search.first.attributes['style'].value.include?("display:none")
276
+ rescue
277
+ false
278
+ end
279
+
280
+ if activate_hidden
281
+ #Activate button is hidden, Deactivate is shown
282
+ if checkbox_account_activity_search.first.attributes['checked'].nil?
283
+ # Not checked
284
+ puts "[ ] IAM access to Account Activity not setup -- Disabled"
285
+ else
286
+ # Checked
287
+ puts "[X] IAM access to Account Activity is setup -- Enabled"
288
+ end
289
+ if usage_report_search.first.attributes['checked'].nil?
290
+ # Not checked
291
+ puts "[ ] IAM access to Usage Reports not setup -- Disabled"
292
+ else
293
+ # Checked
294
+ puts "[X] IAM access to Usage Reports is setup -- Enabled"
295
+ end
296
+ elsif deactivate_hidden
297
+ #Deactivate button is hidden, Activate shown
298
+ if checkbox_account_activity_search.first.attributes['checked'].nil?
299
+ # Not checked
300
+ puts "[X] IAM access to Account Activity is setup -- Enabled"
301
+ else
302
+ # Checked
303
+ puts "[ ] IAM access to Account Activity not setup -- Disabled"
304
+ end
305
+ if usage_report_search.first.attributes['checked'].nil?
306
+ # Not checked
307
+ puts "[X] IAM access to Usage Reports is setup -- Enabled"
308
+ else
309
+ # Checked
310
+ puts "[ ] IAM access to Usage Reports not setup -- Disabled"
311
+ end
312
+ else
313
+ puts "[ ] Could not get status of IAM Usage report & Account activity checkboxes - Unknown"
314
+ end
315
+ rescue => e
316
+ warning(e)
317
+ end
318
+ end
319
+
320
+ def setup_checkboxes
321
+ checkbox_setup_error = " We were unable to enable account activity & usage reports.\n This setting can be enabled manually under the Manage Your Account from the AWS account page."
322
+ begin
323
+ puts "Enabling account activity & usage reports..."
324
+ page = @browser.get("https://portal.aws.amazon.com/gp/aws/manageYourAccount?action=updateIAMUserAccess&activateaa=1&activateur=1")
325
+ json = JSON.parse(page.body)
326
+ if json["error"] != "0"
327
+ puts checkbox_setup_error
328
+ else
329
+ puts " Activity & Usage reports enabled."
330
+ end
331
+ rescue => e
332
+ puts checkbox_setup_error
333
+ warning(e)
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,32 @@
1
+ class Setup
2
+ def self.write_csv(accounts, filename)
3
+ file_exists = File.exists?(filename)
4
+ mode = if file_exists
5
+ if @overwrite_file
6
+ puts "CSV Output file #{filename} exists! Overwriting the file per --overwrite."
7
+ "wb"
8
+ else
9
+ puts "CSV Output file #{filename} exists! Appending to existing file."
10
+ "ab"
11
+ end
12
+ else
13
+ "wb"
14
+ end
15
+
16
+ count = 0
17
+ CSV.open(filename, mode) do |csv|
18
+ unless mode == "ab"
19
+ csv << ["Account ID", "Console URL", "IAM Username", "IAM Password", "AWS Access Key", "AWS Access Secret", "S3 Bucket", "Is Consolidated?"]
20
+ end
21
+ accounts.each do |account|
22
+ count += 1
23
+ csv << [account[:account_id], account[:account_url], account[:user], account[:user_pass], account[:access_key], account[:secret_key], account[:s3_bucket], account[:consolidated]]
24
+ end
25
+ end
26
+ puts "Finished setting up #{count} account(s). CSV File path: #{filename}"
27
+ end
28
+
29
+ def response
30
+ @created_account
31
+ end
32
+ end
@@ -0,0 +1,108 @@
1
+ class Setup
2
+ def bucket_has_policy
3
+ begin
4
+ @s3.get_bucket_policy(@setup_bucket)
5
+ true
6
+ rescue
7
+ false
8
+ end
9
+ end
10
+
11
+ def user_has_policy
12
+ begin
13
+ @iam.get_user_policy("CHTRoPolicy", @aws_ro_name)
14
+ true
15
+ rescue
16
+ false
17
+ end
18
+ end
19
+
20
+ def bucket_policy
21
+ { "Version" => "2008-10-17",
22
+ "Id" => "Policy1335892530063",
23
+ "Statement" => [
24
+ {
25
+ "Sid" => "Stmt1335892150622",
26
+ "Effect" => "Allow",
27
+ "Principal" => {
28
+ "AWS" => "arn:aws:iam::386209384616:root"
29
+ },
30
+ "Action" => ["s3:GetBucketAcl", "s3:GetBucketPolicy"],
31
+ "Resource" => "arn:aws:s3:::#{@setup_bucket}"
32
+ },
33
+ {
34
+ "Sid" => "Stmt1335892526596",
35
+ "Effect" => "Allow",
36
+ "Principal" => {
37
+ "AWS" => "arn:aws:iam::386209384616:root"
38
+ },
39
+ "Action" => ["s3:PutObject"],
40
+ "Resource" => "arn:aws:s3:::#{@setup_bucket}/*"
41
+ }
42
+ ]
43
+ }
44
+ end
45
+ def aws_ro_policy
46
+ {
47
+ "Statement" => [
48
+ {
49
+ "Effect" => "Allow",
50
+ "Action" => [
51
+ "aws-portal:ViewBilling",
52
+ "aws-portal:ViewUsage",
53
+ "autoscaling:Describe*",
54
+ "cloudformation:ListStacks",
55
+ "cloudformation:ListStackResources",
56
+ "cloudformation:DescribeStacks",
57
+ "cloudformation:DescribeStackEvents",
58
+ "cloudformation:DescribeStackResources",
59
+ "cloudformation:GetTemplate",
60
+ "cloudfront:Get*",
61
+ "cloudfront:List*",
62
+ "cloudwatch:Describe*",
63
+ "cloudwatch:Get*",
64
+ "cloudwatch:List*",
65
+ "dynamodb:DescribeTable",
66
+ "dynamodb:ListTables",
67
+ "ec2:Describe*",
68
+ "elasticache:Describe*",
69
+ "elasticbeanstalk:Check*",
70
+ "elasticbeanstalk:Describe*",
71
+ "elasticbeanstalk:List*",
72
+ "elasticbeanstalk:RequestEnvironmentInfo",
73
+ "elasticbeanstalk:RetrieveEnvironmentInfo",
74
+ "elasticloadbalancing:Describe*",
75
+ "iam:List*",
76
+ "iam:Get*",
77
+ "redshift:Describe*",
78
+ "route53:Get*",
79
+ "route53:List*",
80
+ "rds:Describe*",
81
+ "s3:List*",
82
+ "sdb:GetAttributes",
83
+ "sdb:List*",
84
+ "sdb:Select*",
85
+ "ses:Get*",
86
+ "ses:List*",
87
+ "sns:Get*",
88
+ "sns:List*",
89
+ "sqs:GetQueueAttributes",
90
+ "sqs:ListQueues",
91
+ "sqs:ReceiveMessage",
92
+ "storagegateway:List*",
93
+ "storagegateway:Describe*"
94
+ ],
95
+ "Resource" => "*"
96
+ },
97
+ {
98
+ "Effect" => "Allow",
99
+ "Action" => [ "s3:Get*","s3:List*" ],
100
+ "Resource" => [
101
+ "arn:aws:s3:::#{@setup_bucket}",
102
+ "arn:aws:s3:::#{@setup_bucket}/*"
103
+ ]
104
+ }
105
+ ]
106
+ }
107
+ end
108
+ end
@@ -0,0 +1,244 @@
1
+ gem "fog", "1.12.1"
2
+ gem "json", "1.8.0"
3
+ gem "excon", "0.23.0"
4
+ gem "mixlib-cli", "1.3.0"
5
+ gem "mechanize", "2.5.1"
6
+ gem "highline", "1.6.19"
7
+ gem "nokogiri", "1.5.8"
8
+
9
+ require "fog"
10
+ require "mixlib/cli"
11
+ require "json"
12
+ require "mechanize"
13
+ require "excon"
14
+ require "highline/import"
15
+ require "securerandom"
16
+ require "csv"
17
+
18
+ # Ruby 1.8.7 hack for compatability
19
+ unless Kernel.respond_to?(:require_relative)
20
+ module Kernel
21
+ def require_relative(path)
22
+ require File.join(File.dirname(caller[0]), path.to_str)
23
+ end
24
+ end
25
+ end
26
+
27
+ require_relative "cht/aws"
28
+ require_relative "cht/mechanize"
29
+ require_relative "cht/policy"
30
+ require_relative "cht/error_handling"
31
+ require_relative "cht/output"
32
+
33
+
34
+ class MyCLI
35
+ include Mixlib::CLI
36
+
37
+ option :output_file,
38
+ :short => "-o OUTFILE",
39
+ :long => "--output-file OUTFILE",
40
+ :default => "./cloudhealth-accounts.csv",
41
+ :description => "Output CSV"
42
+
43
+ option :overwrite_file,
44
+ :long => "--overwrite",
45
+ :boolean => true,
46
+ :description => "Overwrite output file if it exists."
47
+
48
+ option :input_file,
49
+ :short => "-i INPUT",
50
+ :long => "--input-file INPUT",
51
+ :description => "INPUT CSV File"
52
+
53
+ option :aws_acct_alias,
54
+ :long => "--account-alias ALIAS",
55
+ :description => "Set an AWS Account alias"
56
+
57
+ option :aws_user,
58
+ :short => "-u USER",
59
+ :long => "--aws-user USER",
60
+ :description => "AWS Username"
61
+
62
+ option :aws_pass,
63
+ :short => "-p PASS",
64
+ :long => "--aws-pass PASS",
65
+ :description => "AWS Password"
66
+
67
+ option :aws_key,
68
+ :short => "-k KEY",
69
+ :long => "--aws-key KEY",
70
+ :description => "AWS Key"
71
+
72
+ option :aws_secret,
73
+ :short => "-s SECRET",
74
+ :long => "--aws-secret SECRET",
75
+ :description => "AWS Secret"
76
+
77
+ option :setup_bucket,
78
+ :short => "-b BUCKET",
79
+ :long => "--setup-billing-bucket BUCKET",
80
+ :description => "Name of billing bucket to create/use."
81
+
82
+ option :aws_ro_name,
83
+ :short => "-r READONLY",
84
+ :long => "--aws-ro-name READONLY",
85
+ :default => "cloudhealth",
86
+ :description => "Name of the Read only account to create in AWS"
87
+
88
+ option :ro_user_exists,
89
+ :long => "--user-exists",
90
+ :boolean => true,
91
+ :description => "AWS Read-only user exists, Perform other setup anyway (CSV will not be complete)"
92
+
93
+ option :verbose,
94
+ :short => "-v",
95
+ :long => "--verbose",
96
+ :description => "Enable verbose output / stack trace errors",
97
+ :boolean => true
98
+
99
+ option :help,
100
+ :short => "-h",
101
+ :long => "--help",
102
+ :description => "Help",
103
+ :on => :tail,
104
+ :boolean => true,
105
+ :show_options => true,
106
+ :exit => 0
107
+
108
+ end
109
+
110
+
111
+ class CsvImport
112
+ def initialize(filename)
113
+ @filename = filename
114
+ end
115
+
116
+ def import_file # Make me work!
117
+ end
118
+ end
119
+
120
+ class Setup
121
+ attr_accessor :aws_key, :aws_secret, :aws_user, :aws_pass, :output_file, :input_file, :aws_url, :setup_bucket, :aws_ro_name, :aws_acct_alias, :account_name, :aws_account_id
122
+
123
+ def initialize(options)
124
+ manual_input = ensure_options(options)
125
+ options.merge!(manual_input)
126
+ @aws_key = options[:aws_key]
127
+ @aws_secret = options[:aws_secret]
128
+ @output_file = options[:output_file]
129
+ @input_file = options[:input_file]
130
+ @aws_user = options[:aws_user]
131
+ @aws_pass = options[:aws_pass]
132
+ @setup_bucket = options[:setup_bucket]
133
+ @aws_ro_name = options[:aws_ro_name]
134
+ @aws_acct_alias = options[:aws_acct_alias]
135
+ @aws_account_id = nil
136
+ @verbose = options[:verbose]
137
+ @overwrite_file = options[:overwrite_file]
138
+ @ro_user_exists = options[:ro_user_exists]
139
+ @created_account = {}
140
+ @iam = iam
141
+ @s3 = s3
142
+ @browser = mech_browser
143
+ @mode = ARGV[0]
144
+ end
145
+
146
+ def ensure_options(input)
147
+ # If things dont exist in the options that are required, or required in a combination.
148
+ output_opts = {}
149
+
150
+ if input[:aws_key].nil?
151
+ output_opts[:aws_key] = ask("Input AWS Key: ") do |q|
152
+ q.responses[:not_valid] = "You must enter a 20 char AWS Access Key ID"
153
+ q.responses[:invalid_type] = "You must enter a 20 char AWS Access Key ID"
154
+ q.validate = lambda {|p| p.length == 20 }
155
+ end
156
+ end
157
+
158
+ if input[:aws_secret].nil?
159
+ output_opts[:aws_secret] = ask("Input AWS Secret: ") do |q|
160
+ q.responses[:not_valid] = "You must enter a 40 char AWS Access Secret Key"
161
+ q.responses[:invalid_type] = "You must enter a 40 char AWS Access Secret Key"
162
+ q.validate = lambda {|p| p.length == 40 }
163
+ end
164
+ end
165
+
166
+ if input[:aws_user].nil?
167
+ output_opts[:aws_user] = ask("Input AWS Email/Username: ") do |q|
168
+ q.responses[:not_valid] = "You must enter a valid Email/Username"
169
+ q.responses[:invalid_type] = "You must enter a valid Email/Username"
170
+ q.validate = lambda {|p| p.length > 5 }
171
+ end
172
+ end
173
+
174
+ if input[:aws_pass].nil?
175
+ output_opts[:aws_pass] = ask("Input AWS Password: ") do |q|
176
+ q.responses[:not_valid] = "You must enter a valid password"
177
+ q.responses[:invalid_type] = "You must enter a valid password"
178
+ q.validate = lambda {|p| p.length > 4 }
179
+ end
180
+ end
181
+
182
+ if input[:setup_bucket].nil?
183
+ output_opts[:setup_bucket] = ask("Input S3 Bucket name to setup for billing: ")
184
+ end
185
+
186
+ output_opts
187
+
188
+ end
189
+
190
+ def self.run
191
+ cli = MyCLI.new
192
+ cli.banner = "Usage: cloudhealth-setup test|install|uninstall (options)"
193
+ cli.parse_options
194
+ accounts_to_setup = []
195
+ accounts_processed = []
196
+ if ARGV[0].nil?
197
+ puts cli.opt_parser
198
+ exit
199
+ end
200
+
201
+ if cli.config[:input_file]
202
+ puts "Starting CloudHealth setup in multi-account setup mode, using input file #{cli.config[:input_file]}"
203
+ accounts_to_setup << CsvImport.import_file(cli.config[:input_file])
204
+ else
205
+ puts "Starting CloudHealth setup in single account mode."
206
+ accounts_to_setup << cli.config
207
+ end
208
+
209
+ accounts_to_setup.each do |account_options|
210
+ new_account = Setup.new(account_options)
211
+
212
+ case ARGV[0]
213
+ when "install"
214
+ new_account.check_iam_credentials
215
+ new_account.check_web_credentials
216
+ new_account.setup_monthly_report
217
+ new_account.setup_s3_bucket
218
+ new_account.setup_prog_access
219
+ new_account.setup_detailed_billing
220
+ new_account.setup_cost_alloc
221
+ new_account.setup_checkboxes
222
+ new_account.setup_ro_user
223
+ new_account.setup_account_alias
224
+ new_account.account_consolidated
225
+ accounts_processed << new_account.response
226
+ when "test"
227
+ new_account.test_monthly_report
228
+ new_account.test_s3_bucket
229
+ new_account.test_prog_access
230
+ new_account.test_detailed_billing
231
+ new_account.test_cost_alloc
232
+ new_account.test_checkboxes
233
+ new_account.test_ro_user
234
+ new_account.test_account_alias
235
+ new_account.test_consolidated
236
+ when "uninstall"
237
+ new_account.uninstall_ro_user
238
+ else
239
+ puts cli.opt_parser
240
+ end
241
+ end
242
+ Setup.write_csv(accounts_processed, cli.config[:output_file]) if ARGV[0] == "install"
243
+ end
244
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudhealth-setup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.10
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - CloudHealth Technologies
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.8
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.5.8
30
+ - !ruby/object:Gem::Dependency
31
+ name: fog
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.12.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.12.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.8.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.8.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: excon
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - '='
68
+ - !ruby/object:Gem::Version
69
+ version: 0.23.0
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - '='
76
+ - !ruby/object:Gem::Version
77
+ version: 0.23.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: mixlib-cli
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - '='
84
+ - !ruby/object:Gem::Version
85
+ version: 1.3.0
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - '='
92
+ - !ruby/object:Gem::Version
93
+ version: 1.3.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: mechanize
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - '='
100
+ - !ruby/object:Gem::Version
101
+ version: 2.5.1
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: 2.5.1
110
+ - !ruby/object:Gem::Dependency
111
+ name: highline
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.6.19
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - '='
124
+ - !ruby/object:Gem::Version
125
+ version: 1.6.19
126
+ description: Configures an Amazon AWS account for use with the CloudHealth service,
127
+ including creating a least privilege read only AWS user and enabling the retrieval
128
+ of cost and usage information.
129
+ email:
130
+ - support@cloudhealthtech.com
131
+ executables:
132
+ - cloudhealth-setup
133
+ - fog
134
+ - nokogiri
135
+ extensions: []
136
+ extra_rdoc_files: []
137
+ files:
138
+ - bin/cloudhealth-setup
139
+ - bin/fog
140
+ - bin/nokogiri
141
+ - lib/cht/aws.rb
142
+ - lib/cht/error_handling.rb
143
+ - lib/cht/mechanize.rb
144
+ - lib/cht/output.rb
145
+ - lib/cht/policy.rb
146
+ - lib/cloudhealth-setup.rb
147
+ - cloudhealth-setup.gemspec
148
+ homepage: http://www.cloudhealthtech.com
149
+ licenses:
150
+ - MIT
151
+ post_install_message:
152
+ rdoc_options: []
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ none: false
157
+ requirements:
158
+ - - ! '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ! '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubyforge_project:
169
+ rubygems_version: 1.8.25
170
+ signing_key:
171
+ specification_version: 3
172
+ summary: Configures an Amazon AWS account for use with the CloudHealth service
173
+ test_files: []
174
+ has_rdoc: false