cloudstrap 0.29.1.pre

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.
@@ -0,0 +1,20 @@
1
+ require 'aws-sdk'
2
+ require 'contracts'
3
+ require_relative 'service'
4
+
5
+ module StackatoLKG
6
+ module Amazon
7
+ class IAM < Service
8
+ Contract None => ::Aws::IAM::Types::User
9
+ def user
10
+ @user ||= call_api(:get_user).user
11
+ end
12
+
13
+ private
14
+
15
+ def client
16
+ Aws::IAM::Client
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require 'aws-sdk'
2
+ require 'contracts'
3
+ require_relative 'support/rate_limit_handler'
4
+ require_relative '../config'
5
+
6
+ module StackatoLKG
7
+ module Amazon
8
+ class Service
9
+ include ::Contracts::Core
10
+ include ::Contracts::Builtin
11
+ include Support::RateLimitHandler
12
+
13
+ Contract Maybe[Config] => Service
14
+ def initialize(config = nil)
15
+ @config = config
16
+ self
17
+ end
18
+
19
+ Contract None => Aws::Client
20
+ def client
21
+ raise NotImplementedError
22
+ end
23
+
24
+ Contract None => Aws::Client
25
+ def api
26
+ @api ||= client.new region: config.region
27
+ end
28
+
29
+ Contract None => Config
30
+ def config
31
+ @config ||= Config.new
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ require 'aws-sdk'
2
+ require 'contracts'
3
+ require 'retries'
4
+
5
+ module StackatoLKG
6
+ module Amazon
7
+ module Support
8
+ module RateLimitHandler
9
+ include ::Contracts::Core
10
+ include ::Contracts::Builtin
11
+
12
+ Contract None => Proc
13
+ def request_limit_exceeded_handler
14
+ Proc.new do |exception, attempt, seconds|
15
+ STDERR.puts "Encountered a #{exception.class}. DON'T PANIC. Waiting and trying again works (usually). Let's do that! (this was attempt #{attempt} after #{seconds} seconds)"
16
+ end
17
+ end
18
+
19
+ Contract Symbol, Args[Any] => Any
20
+ def call_api(method, *args)
21
+ with_retries(
22
+ rescue: Aws::EC2::Errors::RequestLimitExceeded,
23
+ handler: request_limit_exceeded_handler,
24
+ base_sleep_seconds: 1.0,
25
+ max_sleep_seconds: 8.0
26
+ ) do
27
+ api.method(method).call(*args)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,461 @@
1
+ require 'contracts'
2
+ require 'moneta'
3
+ require 'securerandom'
4
+
5
+ require_relative 'amazon'
6
+ require_relative 'config'
7
+ require_relative 'hdp/bootstrap_properties'
8
+ require_relative 'ssh'
9
+
10
+ module StackatoLKG
11
+ class BootstrapAgent
12
+ include ::Contracts::Core
13
+ include ::Contracts::Builtin
14
+
15
+ Contract None => String
16
+ def create_vpc
17
+ cache.store(:vpc_id, ec2.create_vpc.vpc_id).tap do |vpc_id|
18
+ ec2.assign_name(bootstrap_tag, vpc_id)
19
+ end
20
+ end
21
+
22
+ Contract None => Maybe[String]
23
+ def find_vpc
24
+ ENV.fetch('BOOTSTRAP_VPC_ID') do
25
+ cache.fetch(:vpc_id) do
26
+ cache.store :vpc_id, ec2
27
+ .tagged(type: 'vpc', value: bootstrap_tag)
28
+ .map(&:resource_id)
29
+ .first
30
+ end
31
+ end
32
+ end
33
+
34
+ Contract None => String
35
+ def internet_gateway
36
+ find_internet_gateway || create_internet_gateway
37
+ end
38
+
39
+ Contract None => String
40
+ def create_internet_gateway
41
+ cache.store(:internet_gateway_id,
42
+ ec2.create_internet_gateway.internet_gateway_id
43
+ ).tap { |internet_gateway_id| ec2.assign_name bootstrap_tag, internet_gateway_id }
44
+ end
45
+
46
+ Contract None => String
47
+ def nat_gateway_ip_allocation
48
+ ENV.fetch('BOOTSTRAP_NAT_GATEWAY_ALLOCATION_ID') do
49
+ cache.fetch(:nat_gateway_allocation_id) do # TODO: Simplify this.
50
+ id = ec2
51
+ .nat_gateways
52
+ .select { |nat_gateway| nat_gateway.vpc_id == vpc }
53
+ .flat_map { |nat_gateway| nat_gateway.nat_gateway_addresses.map { |address| address.allocation_id } }
54
+ .first || ec2.unassociated_address || ec2.create_address
55
+ cache.store(:nat_gateway_allocation_id, id)
56
+ end
57
+ end
58
+ end
59
+
60
+ Contract None => Maybe[String]
61
+ def find_nat_gateway
62
+ ec2
63
+ .nat_gateways
64
+ .select { |nat_gateway| nat_gateway.vpc_id == vpc }
65
+ .reject { |nat_gateway| %w(failed deleted).include? nat_gateway.state }
66
+ .map { |nat_gateway| nat_gateway.nat_gateway_id }
67
+ .first
68
+ end
69
+
70
+ Contract None => String
71
+ def create_nat_gateway
72
+ attach_gateway unless ec2.internet_gateway_attached?(internet_gateway, vpc)
73
+ ec2.create_nat_gateway(public_subnet, nat_gateway_ip_allocation).nat_gateway_id
74
+ end
75
+
76
+ Contract None => String
77
+ def nat_gateway
78
+ ENV.fetch('BOOTSTRAP_NAT_GATEWAY_ID') do
79
+ cache.fetch(:nat_gateway_id) do
80
+ cache.store(:nat_gateway_id, (find_nat_gateway || create_nat_gateway))
81
+ end
82
+ end
83
+ end
84
+
85
+ Contract None => Maybe[String]
86
+ def find_internet_gateway
87
+ ENV.fetch('BOOTSTRAP_INTERNET_GATEWAY_ID') do
88
+ cache.fetch(:internet_gateway_id) do
89
+ find_tagged_internet_gateway || find_internet_gateway_for_vpc
90
+ end
91
+ end
92
+ end
93
+
94
+ Contract None => Maybe[String]
95
+ def find_tagged_internet_gateway
96
+ ec2
97
+ .tagged(type: 'internet-gateway', value: bootstrap_tag)
98
+ .map { |resource| resource.resource.id }
99
+ .first
100
+ end
101
+
102
+ Contract None => Maybe[String]
103
+ def find_internet_gateway_for_vpc
104
+ ec2
105
+ .internet_gateways
106
+ .select { |gateway| gateway.attachments.any? { |attachment| attachment.vpc_id == vpc } }
107
+ .map { |gateway| gateway.internet_gateway_id }
108
+ .first
109
+ end
110
+
111
+ Contract None => String
112
+ def create_jumpbox_security_group
113
+ cache.store(:jumpbox_security_group, ec2.create_security_group(:jumpbox, vpc)).tap do |sg|
114
+ ec2.assign_name(bootstrap_tag, sg)
115
+ end
116
+ end
117
+
118
+ Contract None => Maybe[String]
119
+ def find_jumpbox_security_group
120
+ @jumpbox_security_group ||= ENV.fetch('BOOTSTRAP_JUMPBOX_SECURITY_GROUP') do
121
+ cache.fetch(:jumpbox_security_group) do
122
+ cache.store :jumpbox_security_group, ec2
123
+ .tagged(type: 'security-group', value: bootstrap_tag)
124
+ .map(&:resource_id)
125
+ .first
126
+ end
127
+ end
128
+ end
129
+
130
+ Contract None => Bool
131
+ def allow_ssh
132
+ ec2.authorize_security_group_ingress :tcp, 22, '0.0.0.0/0', jumpbox_security_group
133
+ end
134
+
135
+ Contract None => String
136
+ def jumpbox_security_group
137
+ find_jumpbox_security_group || create_jumpbox_security_group
138
+ end
139
+
140
+ Contract None => String
141
+ def private_subnet
142
+ @private_subnet ||= ENV.fetch('BOOTSTRAP_PRIVATE_SUBNET_ID') do
143
+ cache.fetch(:private_subnet_id) do
144
+ properties = { vpc_id: vpc, cidr_block: config.private_cidr_block }
145
+ cache.store(:private_subnet_id, (ec2.subnet(properties) || ec2.create_subnet(properties)).tap do |subnet|
146
+ ec2.assign_name bootstrap_tag, subnet.subnet_id unless subnet.tags.any? do |tag|
147
+ tag.key == 'Name' && tag.value = bootstrap_tag
148
+ end
149
+ end.subnet_id)
150
+ end
151
+ end
152
+ end
153
+
154
+ Contract None => String
155
+ def public_subnet
156
+ @public_subnet ||= ENV.fetch('BOOTSTRAP_PUBLIC_SUBNET_ID') do
157
+ cache.fetch(:public_subnet_id) do
158
+ properties = { vpc_id: vpc, cidr_block: config.public_cidr_block }
159
+ cache.store(:public_subnet_id, (ec2.subnet(properties) || ec2.create_subnet(properties)).tap do |subnet|
160
+ ec2.assign_name bootstrap_tag, subnet.subnet_id unless subnet.tags.any? do |tag|
161
+ tag.key == 'Name' && tag.value = bootstrap_tag
162
+ end
163
+ end.subnet_id)
164
+ end
165
+ end
166
+ end
167
+
168
+ Contract None => String
169
+ def route_table
170
+ @route_table ||= ENV.fetch('BOOTSTRAP_ROUTE_TABLE_ID') do
171
+ cache.fetch(:route_table_id) do
172
+ cache.store(:route_table_id, ec2
173
+ .route_tables
174
+ .select { |route_table| route_table.vpc_id == vpc }
175
+ .select { |route_table| route_table.associations.any? { |association| association.main } }
176
+ .map { |route_table| route_table.route_table_id }
177
+ .first).tap do |route_table_id|
178
+ ec2.assign_name bootstrap_tag, route_table_id
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ Contract None => String
185
+ def private_route_table
186
+ @private_route_table ||= ENV.fetch('BOOTSTRAP_PRIVATE_ROUTE_TABLE_ID') do
187
+ cache.fetch(:private_route_table_id) do
188
+ id = ec2
189
+ .route_tables
190
+ .select { |route_table| route_table.vpc_id == vpc }
191
+ .reject { |route_table| route_table.associations.any? { |association| association.main } }
192
+ .map { |route_table| route_table.route_table_id }
193
+ .first || ec2.create_route_table(vpc).route_table_id
194
+ cache.store(:private_route_table_id, id).tap do |private_route_table_id|
195
+ ec2.assign_name bootstrap_tag, private_route_table_id
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ Contract None => Bool
202
+ def attach_gateway
203
+ ec2.attach_internet_gateway internet_gateway, vpc # TODO: Cache this
204
+ end
205
+
206
+ Contract None => Bool
207
+ def default_route
208
+ ec2.create_route('0.0.0.0/0', internet_gateway, route_table) # TODO: Cache this
209
+ end
210
+
211
+ Contract None => Bool
212
+ def nat_route
213
+ ec2.create_route('0.0.0.0/0', nat_gateway, private_route_table) # TODO: Cache this
214
+ end
215
+
216
+ Contract None => String
217
+ def nat_route_association
218
+ @nat_route_association || ENV.fetch('BOOTSTRAP_NAT_ROUTE_ASSOCIATION_ID') do
219
+ cache.fetch(:nat_route_association_id) do
220
+ cache.store(:nat_route_association_id, ec2.associate_route_table(private_route_table, private_subnet))
221
+ end
222
+ end
223
+ end
224
+
225
+ Contract None => ArrayOf[String]
226
+ def subnets
227
+ [public_subnet, private_subnet]
228
+ end
229
+
230
+ Contract None => Bool
231
+ def enable_public_ips
232
+ ec2.map_public_ip_on_launch?(public_subnet) || ec2.map_public_ip_on_launch(public_subnet, true)
233
+ end
234
+
235
+ Contract None => String
236
+ def vpc
237
+ find_vpc || create_vpc
238
+ end
239
+
240
+ Contract None => String
241
+ def create_jumpbox
242
+ upload_ssh_key
243
+
244
+ cache.store(:jumpbox_id, ec2.create_instance(
245
+ image_id: ami,
246
+ instance_type: config.instance_type,
247
+ key_name: bootstrap_tag,
248
+ client_token: Digest::SHA256.hexdigest(bootstrap_tag),
249
+ network_interfaces: [{
250
+ device_index: 0,
251
+ subnet_id: public_subnet,
252
+ associate_public_ip_address: true,
253
+ groups: [jumpbox_security_group]
254
+ }]
255
+ ).instance_id).tap do |instance_id|
256
+ ec2.assign_name bootstrap_tag, instance_id
257
+ end
258
+ end
259
+
260
+ Contract None => Maybe[String]
261
+ def find_jumpbox
262
+ ENV.fetch('BOOTSTRAP_JUMPBOX_ID') do
263
+ cache.fetch(:jumpbox_id) do
264
+ ec2
265
+ .tagged(type: 'instance', value: bootstrap_tag)
266
+ .map(&:resource_id)
267
+ .first
268
+ end
269
+ end
270
+ end
271
+
272
+ Contract None => String
273
+ def jumpbox
274
+ find_jumpbox || create_jumpbox
275
+ end
276
+
277
+ Contract None => String
278
+ def ami
279
+ @ami ||= ENV.fetch('BOOTSTRAP_AMI') do
280
+ cache.fetch(:ami_id) do
281
+ cache.store :ami_id, ec2.latest_ubuntu(config.ubuntu_release).image_id
282
+ end
283
+ end
284
+ end
285
+
286
+ Contract None => String
287
+ def upload_ssh_key
288
+ ec2.import_key_pair bootstrap_tag, ssh_key.to_s # TODO: Cache this.
289
+ end
290
+
291
+ Contract None => SSH::Key
292
+ def ssh_key
293
+ @ssh_key ||= SSH::Key.new bootstrap_tag
294
+ end
295
+
296
+ Contract None => String
297
+ def bootstrap_tag
298
+ @bootstrap_tag ||= ENV.fetch('BOOTSTRAP_TAG') do
299
+ "lkg@#{username}/#{uuid}"
300
+ end
301
+ end
302
+
303
+ Contract None => String
304
+ def username
305
+ @username ||= ENV.fetch('BOOTSTRAP_USERNAME') do
306
+ cache.fetch(:username) do
307
+ cache.store(:username, iam.user.user_name)
308
+ end
309
+ end
310
+ end
311
+
312
+ Contract None => String
313
+ def uuid
314
+ @uuid ||= ENV.fetch('BOOTSTRAP_UUID') do
315
+ cache.fetch(:uuid) do
316
+ cache.store(:uuid, SecureRandom.uuid)
317
+ end
318
+ end
319
+ end
320
+
321
+ Contract None => String
322
+ def public_availability_zone
323
+ @public_availability_zone ||= ENV.fetch('BOOTSTRAP_PUBLIC_AVAILABILITY_ZONE') do
324
+ cache.fetch(:public_availability_zone) do
325
+ cache.store(:public_availability_zone, ec2
326
+ .subnets
327
+ .select { |subnet| subnet.subnet_id == public_subnet }
328
+ .map { |subnet| subnet.availability_zone }
329
+ .first)
330
+ end
331
+ end
332
+ end
333
+
334
+ Contract None => String
335
+ def private_availability_zone
336
+ @private_availability_zone ||= ENV.fetch('BOOTSTRAP_PRIVATE_AVAILABILITY_ZONE') do
337
+ cache.fetch(:private_availability_zone) do
338
+ cache.store(:private_availability_zone, ec2
339
+ .subnets
340
+ .select { |subnet| subnet.subnet_id == private_subnet }
341
+ .map { |subnet| subnet.availability_zone }
342
+ .first)
343
+ end
344
+ end
345
+ end
346
+
347
+ Contract None => String
348
+ def jumpbox_ip
349
+ @jumpbox_ip ||= ENV.fetch('BOOTSTRAP_JUMPBOX_IP') do
350
+ cache.fetch(:jumpbox_ip) do
351
+ cache.store(:jumpbox_ip, ec2
352
+ .instances
353
+ .select { |instance| instance.instance_id == jumpbox }
354
+ .flat_map(&:network_interfaces)
355
+ .map(&:association)
356
+ .map(&:public_ip)
357
+ .first)
358
+ end
359
+ end
360
+ end
361
+
362
+ Contract None => Bool
363
+ def configure_hdp
364
+ bootstrap_properties
365
+ .update('Provider', 'AWS')
366
+ .update('AWS.Region', config.region)
367
+ .update('AWS.AvailabilityZones', public_availability_zone)
368
+ .update('AWS.PublicSubnetIDsAndAZ', [public_subnet, public_availability_zone].join(':'))
369
+ .update('AWS.PrivateSubnetIDsAndAZ', [private_subnet, private_availability_zone].join(':'))
370
+ .update('AWS.Keypair', bootstrap_tag)
371
+ .update('AWS.KeypairFile', '/home/ubuntu/.ssh/id_rsa')
372
+ .update('AWS.JumpboxCIDR', '0.0.0.0/0')
373
+ .update('AWS.VPCID', vpc)
374
+ .update('AWS.LinuxAMI', ami)
375
+ .save!
376
+ end
377
+
378
+ Contract None => Bool
379
+ def jumpbox_running?
380
+ ec2
381
+ .instances
382
+ .select { |instance| instance.instance_id == jumpbox }
383
+ .map { |instance| instance.state.name }
384
+ .first == 'running'
385
+ end
386
+
387
+ Contract None => Any
388
+ def configure_jumpbox
389
+ private_key = ssh_key.private_file
390
+ properties = bootstrap_properties.file
391
+ package = config.hdp_package_url
392
+
393
+ ssh.to(jumpbox_ip) do
394
+ '/home/ubuntu/.ssh/id_rsa'.tap do |target|
395
+ execute :rm, '-f', target
396
+ upload! private_key, target
397
+ execute :chmod, '-w', target
398
+ end
399
+
400
+ upload! properties, '/home/ubuntu/bootstrap.properties'
401
+
402
+ as :root do
403
+ execute :apt, *%w(install --assume-yes genisoimage aria2)
404
+ execute :aria2c, '--continue=true', '--dir=/opt', '--out=bootstrap.deb', package
405
+ execute :dpkg, *%w(--install /opt/bootstrap.deb)
406
+ end
407
+ end
408
+ end
409
+
410
+ Contract None => Bool
411
+ def requires_human_oversight?
412
+ ['false', 'nil', nil].include? ENV['BOOTSTRAP_WITHOUT_HUMAN_OVERSIGHT']
413
+ end
414
+
415
+ Contract None => Any
416
+ def launch
417
+ return false if requires_human_oversight?
418
+
419
+ access_key_id = ec2.api.config.credentials.credentials.access_key_id
420
+ secret_access_key = ec2.api.config.credentials.credentials.secret_access_key
421
+
422
+ ssh.to(jumpbox_ip) do
423
+ with(aws_access_key_id: access_key_id, aws_secret_access_key: secret_access_key) do
424
+ execute :bootstrap, *%w(install bootstrap.properties)
425
+ end
426
+ end
427
+ end
428
+
429
+ private
430
+
431
+ Contract None => SSH::Client
432
+ def ssh
433
+ @ssh ||= SSH::Client.new(ssh_key.private_file)
434
+ end
435
+
436
+ Contract None => HDP::BootstrapProperties
437
+ def bootstrap_properties
438
+ @hdp ||= HDP::BootstrapProperties.new
439
+ end
440
+
441
+ Contract None => Amazon::EC2
442
+ def ec2
443
+ @ec2 ||= Amazon::EC2.new
444
+ end
445
+
446
+ Contract None => Amazon::IAM
447
+ def iam
448
+ @iam ||= Amazon::IAM.new
449
+ end
450
+
451
+ Contract None => Config
452
+ def config
453
+ @config ||= Config.new
454
+ end
455
+
456
+ Contract None => Moneta::Proxy
457
+ def cache
458
+ @cache ||= Moneta.new :File, dir: config.cache_path
459
+ end
460
+ end
461
+ end