ec2launcher 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/bin/ec2launcher +34 -0
- data/ec2launcher.gemspec +18 -0
- data/lib/ec2launcher/application.rb +262 -0
- data/lib/ec2launcher/block_device.rb +91 -0
- data/lib/ec2launcher/block_device_builder.rb +270 -0
- data/lib/ec2launcher/config.rb +76 -0
- data/lib/ec2launcher/defaults.rb +9 -0
- data/lib/ec2launcher/email_notification.rb +62 -0
- data/lib/ec2launcher/environment.rb +179 -0
- data/lib/ec2launcher/init_options.rb +166 -0
- data/lib/ec2launcher/version.rb +6 -0
- data/lib/ec2launcher.rb +734 -0
- data/startup-scripts/setup.rb +154 -0
- data/startup-scripts/setup_instance.rb +340 -0
- metadata +81 -0
data/lib/ec2launcher.rb
ADDED
@@ -0,0 +1,734 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2012 Sean Laurent
|
3
|
+
#
|
4
|
+
require 'rubygems'
|
5
|
+
require 'optparse'
|
6
|
+
require 'ostruct'
|
7
|
+
require 'aws-sdk'
|
8
|
+
|
9
|
+
require "ec2launcher/version"
|
10
|
+
require "ec2launcher/config"
|
11
|
+
require "ec2launcher/defaults"
|
12
|
+
|
13
|
+
require 'ec2launcher/application'
|
14
|
+
require 'ec2launcher/environment'
|
15
|
+
require 'ec2launcher/block_device_builder'
|
16
|
+
|
17
|
+
module EC2Launcher
|
18
|
+
class AmiDetails
|
19
|
+
attr_reader :ami_name, :ami_id
|
20
|
+
|
21
|
+
def initialize(name, id)
|
22
|
+
@ami_name = name
|
23
|
+
@ami_id = id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Launcher
|
28
|
+
# Runs an AWS request inside a Ruby block with an exponential backoff in case
|
29
|
+
# we exceed the allowed AWS RequestLimit.
|
30
|
+
#
|
31
|
+
# @param [Integer] max_time maximum amount of time to sleep before giving up.
|
32
|
+
# @param [Integer] sleep_time the initial amount of time to sleep before retrying.
|
33
|
+
# @param [message] message message to display if we get an exception.
|
34
|
+
# @param [Block] block Ruby code block to execute.
|
35
|
+
def run_with_backoff(max_time, sleep_time, message, &block)
|
36
|
+
if sleep_time > max_time
|
37
|
+
puts "AWS::EC2::Errors::RequestLimitExceeded ... failed #{message}"
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
yield
|
43
|
+
rescue AWS::EC2::Errors::RequestLimitExceeded
|
44
|
+
puts "AWS::EC2::Errors::RequestLimitExceeded ... retrying #{message} in #{sleep_time} seconds"
|
45
|
+
sleep sleep_time
|
46
|
+
run_with_backoff(max_time, sleep_time * 2, message, &block)
|
47
|
+
end
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def launch(options)
|
52
|
+
@options = options
|
53
|
+
|
54
|
+
# Load configuration data
|
55
|
+
@config = load_config_file
|
56
|
+
|
57
|
+
environments_directories = process_directory_list(@config.environments, "environments", "Environments", false)
|
58
|
+
applications_directories = process_directory_list(@config.applications, "applications", "Applications", true)
|
59
|
+
|
60
|
+
# Attempt to load default environment data
|
61
|
+
@default_environment = nil
|
62
|
+
environments_directories.each do |env_dir|
|
63
|
+
filename = File.join(env_dir, "default.rb")
|
64
|
+
@default_environment = load_environment_file(filename)
|
65
|
+
unless @default_environment.nil?
|
66
|
+
validate_environment(filename, @default_environment)
|
67
|
+
break
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@default_environment ||= EC2Launcher::Environment.new
|
71
|
+
|
72
|
+
# Load other environments
|
73
|
+
@environments = { }
|
74
|
+
environments_directories.each do |env_dir|
|
75
|
+
Dir.entries(env_dir).each do |env_name|
|
76
|
+
filename = File.join(env_dir, env_name)
|
77
|
+
next if File.directory?(filename)
|
78
|
+
next if filename == "default.rb"
|
79
|
+
|
80
|
+
new_env = load_environment_file(filename, @default_environment)
|
81
|
+
validate_environment(filename, new_env)
|
82
|
+
|
83
|
+
@environments[new_env.name] = new_env
|
84
|
+
new_env.aliases.each {|env_alias| @environments[env_alias] = new_env }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Load applications
|
89
|
+
@applications = {}
|
90
|
+
applications_directories.each do |app_dir|
|
91
|
+
Dir.entries(app_dir).each do |application_name|
|
92
|
+
filename = File.join(app_dir, application_name)
|
93
|
+
next if File.directory?(filename)
|
94
|
+
|
95
|
+
apps = ApplicationDSL.execute(File.read(filename)).applications
|
96
|
+
apps.each do |new_application|
|
97
|
+
@applications[new_application.name] = new_application
|
98
|
+
validate_application(filename, new_application)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Process inheritance rules for applications
|
104
|
+
@applications.values.each do |app|
|
105
|
+
next if app.inherit.nil?
|
106
|
+
|
107
|
+
# Find base application
|
108
|
+
base_app = @applications[app.inherit]
|
109
|
+
abort("Invalid inheritance '#{app.inherit}' in #{app.name}") if base_app.nil?
|
110
|
+
|
111
|
+
# Clone base application
|
112
|
+
new_app = Marshal::load(Marshal.dump(base_app))
|
113
|
+
new_app.merge(app)
|
114
|
+
@applications[new_app.name] = new_app
|
115
|
+
end
|
116
|
+
|
117
|
+
if @options.list
|
118
|
+
puts ""
|
119
|
+
env_names = @environments.keys.sort.join(", ")
|
120
|
+
puts "Environments: #{env_names}"
|
121
|
+
|
122
|
+
app_names = @applications.keys.sort.join(", ")
|
123
|
+
puts "Applications: #{app_names}"
|
124
|
+
exit 0
|
125
|
+
end
|
126
|
+
|
127
|
+
##############################
|
128
|
+
# ENVIRONMENT
|
129
|
+
##############################
|
130
|
+
unless @environments.has_key? options.environ
|
131
|
+
puts "Environment not found: #{options.environ}"
|
132
|
+
exit 2
|
133
|
+
end
|
134
|
+
@environment = @environments[options.environ]
|
135
|
+
|
136
|
+
##############################
|
137
|
+
# APPLICATION
|
138
|
+
##############################
|
139
|
+
unless @applications.has_key? options.application
|
140
|
+
puts "Application not found: #{options.application}"
|
141
|
+
exit 3
|
142
|
+
end
|
143
|
+
@application = @applications[options.application]
|
144
|
+
|
145
|
+
# Initialize AWS and create EC2 connection
|
146
|
+
initialize_aws()
|
147
|
+
@ec2 = AWS::EC2.new
|
148
|
+
|
149
|
+
##############################
|
150
|
+
# AVAILABILITY ZONES
|
151
|
+
##############################
|
152
|
+
availability_zone = options.zone
|
153
|
+
if availability_zone.nil?
|
154
|
+
availability_zone = @application.availability_zone
|
155
|
+
availability_zone ||= @environment.availability_zone
|
156
|
+
availability_zone ||= @default_environment.availability_zone
|
157
|
+
availability_zone ||= "us-east-1a"
|
158
|
+
end
|
159
|
+
|
160
|
+
##############################
|
161
|
+
# SSH KEY
|
162
|
+
##############################
|
163
|
+
key_name = @environment.key_name
|
164
|
+
key_name ||= @default_environment.key_name
|
165
|
+
if key_name.nil?
|
166
|
+
puts "Unable to determine SSH key name."
|
167
|
+
exit 4
|
168
|
+
end
|
169
|
+
|
170
|
+
##############################
|
171
|
+
# SECURITY GROUPS
|
172
|
+
##############################
|
173
|
+
security_groups = []
|
174
|
+
security_groups += @environment.security_groups unless @environment.security_groups.nil?
|
175
|
+
security_groups += @application.security_groups_for_environment(options.environ)
|
176
|
+
|
177
|
+
##############################
|
178
|
+
# INSTANCE TYPE
|
179
|
+
##############################
|
180
|
+
instance_type = options.instance_type
|
181
|
+
instance_type ||= @application.instance_type
|
182
|
+
instance_type ||= "m1.small"
|
183
|
+
|
184
|
+
##############################
|
185
|
+
# ARCHITECTURE
|
186
|
+
##############################
|
187
|
+
instance_architecture = "x86_64"
|
188
|
+
|
189
|
+
instance_virtualization = case instance_type
|
190
|
+
when "cc1.4xlarge" then "hvm"
|
191
|
+
when "cc2.8xlarge" then "hvm"
|
192
|
+
when "cg1.4xlarge" then "hvm"
|
193
|
+
else "paravirtual"
|
194
|
+
end
|
195
|
+
|
196
|
+
##############################
|
197
|
+
# AMI
|
198
|
+
##############################
|
199
|
+
ami_name_match = @application.ami_name
|
200
|
+
ami_name_match ||= @environment.ami_name
|
201
|
+
ami = find_ami(instance_architecture, instance_virtualization, ami_name_match, @options.ami_id)
|
202
|
+
|
203
|
+
##############################
|
204
|
+
# HOSTNAME
|
205
|
+
##############################
|
206
|
+
hostname = @options.hostname
|
207
|
+
if hostname.nil?
|
208
|
+
short_hostname = generate_hostname()
|
209
|
+
hostname = short_hostname
|
210
|
+
|
211
|
+
unless @environment.domain_name.nil?
|
212
|
+
hostname += ".#{@environment.domain_name}"
|
213
|
+
end
|
214
|
+
else
|
215
|
+
short_hostname = hostname
|
216
|
+
unless @environment.domain_name.nil?
|
217
|
+
short_hostname = hostname.gsub(/.#{@environment.domain_name}/, '')
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
##############################
|
222
|
+
# Block devices
|
223
|
+
##############################
|
224
|
+
builder = EC2Launcher::BlockDeviceBuilder.new(@ec2, @options.volume_size)
|
225
|
+
builder.generate_block_devices(hostname, short_hostname, instance_type, @environment, @application, @options.clone_host)
|
226
|
+
block_device_mappings = builder.block_device_mappings
|
227
|
+
block_device_tags = builder.block_device_tags
|
228
|
+
|
229
|
+
##############################
|
230
|
+
# ELB
|
231
|
+
##############################
|
232
|
+
elb_name = nil
|
233
|
+
elb_name = @application.elb_for_environment(@environment.name) unless @application.elb.nil?
|
234
|
+
|
235
|
+
##############################
|
236
|
+
# Roles
|
237
|
+
##############################
|
238
|
+
roles = []
|
239
|
+
roles += @environment.roles unless @environment.roles.nil?
|
240
|
+
roles += @application.roles_for_environment(@environment.name)
|
241
|
+
|
242
|
+
##############################
|
243
|
+
# Packages - preinstall
|
244
|
+
##############################
|
245
|
+
subnet = nil
|
246
|
+
subnet = @application.subnet unless @application.subnet.nil?
|
247
|
+
subnet ||= @environment.subnet unless @environment.subnet.nil?
|
248
|
+
|
249
|
+
##############################
|
250
|
+
# Gems - preinstall
|
251
|
+
##############################
|
252
|
+
gems = []
|
253
|
+
gems += @environment.gems unless @environment.gems.nil?
|
254
|
+
gems += @application.gems unless @application.gems.nil?
|
255
|
+
|
256
|
+
##############################
|
257
|
+
# Packages - preinstall
|
258
|
+
##############################
|
259
|
+
packages = []
|
260
|
+
packages += @environment.packages unless @environment.packages.nil?
|
261
|
+
packages += @application.packages unless @application.packages.nil?
|
262
|
+
|
263
|
+
##############################
|
264
|
+
# Email Notification
|
265
|
+
##############################
|
266
|
+
email_notifications = nil
|
267
|
+
email_notifications = @application.email_notifications
|
268
|
+
email_notifications ||= @environment.email_notifications
|
269
|
+
|
270
|
+
##############################
|
271
|
+
# Chef Validation PEM
|
272
|
+
##############################
|
273
|
+
chef_validation_pem_url = nil
|
274
|
+
chef_validation_pem_url = @options.chef_validation_url
|
275
|
+
chef_validation_pem_url ||= @environment.chef_validation_pem_url
|
276
|
+
|
277
|
+
##############################
|
278
|
+
# File on new instance containing AWS keys
|
279
|
+
##############################
|
280
|
+
aws_keyfile = @environment.aws_keyfile
|
281
|
+
|
282
|
+
##############################
|
283
|
+
# Build JSON for setup scripts
|
284
|
+
##############################
|
285
|
+
setup_json = {
|
286
|
+
'hostname' => hostname,
|
287
|
+
'short_hostname' => short_hostname,
|
288
|
+
'roles' => roles,
|
289
|
+
'chef_server_url' => @environment.chef_server_url,
|
290
|
+
'chef_validation_pem_url' => chef_validation_pem_url,
|
291
|
+
'aws_keyfile' => aws_keyfile,
|
292
|
+
'gems' => gems,
|
293
|
+
'packages' => packages
|
294
|
+
}
|
295
|
+
unless @application.block_devices.empty?
|
296
|
+
setup_json['block_devices'] = @application.block_devices
|
297
|
+
end
|
298
|
+
unless email_notifications.nil?
|
299
|
+
setup_json['email_notifications'] = email_notifications
|
300
|
+
end
|
301
|
+
|
302
|
+
##############################
|
303
|
+
# Build launch command
|
304
|
+
user_data = "#!/bin/sh
|
305
|
+
export HOME=/root
|
306
|
+
echo '#{setup_json.to_json}' > /tmp/setup.json
|
307
|
+
curl http://bazaar.launchpad.net/~alestic/runurl/trunk/download/head:/runurl-20090817053347-o2e56z7xwq8m9tt6-1/runurl -o /tmp/runurl
|
308
|
+
chmod +x /tmp/runurl
|
309
|
+
/tmp/runurl https://s3.amazonaws.com/startup-scripts/setup.rb -e #{options.environ} -a #{options.application} -h #{hostname} /tmp/setup.json > /var/log/cloud-startup.log
|
310
|
+
rm -f /tmp/runurl"
|
311
|
+
user_data += " -c #{options.clone_host}" unless options.clone_host.nil?
|
312
|
+
|
313
|
+
# Add extra requested commands to the launch sequence
|
314
|
+
options.commands.each {|extra_cmd| user_data += "\n#{extra_cmd}" }
|
315
|
+
|
316
|
+
##############################
|
317
|
+
puts
|
318
|
+
puts "Availability zone: #{availability_zone}"
|
319
|
+
puts "Key name : #{key_name}"
|
320
|
+
puts "Security groups : #{security_groups.join(", ")}"
|
321
|
+
puts "Instance type : #{instance_type}"
|
322
|
+
puts "Architecture : #{instance_architecture}"
|
323
|
+
puts "AMI name : #{ami.ami_name}"
|
324
|
+
puts "AMI id : #{ami.ami_id}"
|
325
|
+
puts "Name : #{hostname}"
|
326
|
+
puts "ELB : #{elb_name}" if elb_name
|
327
|
+
puts "Chef PEM : #{chef_validation_pem_url}"
|
328
|
+
puts "AWS key file : #{aws_keyfile}"
|
329
|
+
puts "Roles : #{roles.join(', ')}"
|
330
|
+
puts "Gems : #{gems.join(', ')}"
|
331
|
+
puts "Packages : #{packages.join(', ')}"
|
332
|
+
puts "VPC Subnet : #{subnet}" if subnet
|
333
|
+
|
334
|
+
unless block_device_mappings.empty?
|
335
|
+
block_device_mappings.keys.sort.each do |key|
|
336
|
+
if block_device_mappings[key] =~ /^ephemeral/
|
337
|
+
puts " Block device : #{key}, #{block_device_mappings[key]}"
|
338
|
+
else
|
339
|
+
puts " Block device : #{key}, #{block_device_mappings[key][:volume_size]}GB, " +
|
340
|
+
"#{block_device_mappings[key][:snapshot_id]}, " +
|
341
|
+
"(#{block_device_mappings[key][:delete_on_termination] ? 'auto-delete' : 'no delete'})"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
puts "User data:"
|
347
|
+
puts user_data
|
348
|
+
puts
|
349
|
+
|
350
|
+
if chef_validation_pem_url.nil?
|
351
|
+
puts "***ERROR*** Missing the URL For the Chef Validation PEM file."
|
352
|
+
exit 3
|
353
|
+
end
|
354
|
+
|
355
|
+
# Quit if we're only displaying the defaults
|
356
|
+
exit 0 if @options.show_defaults
|
357
|
+
|
358
|
+
##############################
|
359
|
+
# Launch the new intance
|
360
|
+
##############################
|
361
|
+
instance = launch_instance(hostname, ami.ami_id, availability_zone, key_name, security_groups, instance_type, user_data, block_device_mappings, block_device_tags, subnet)
|
362
|
+
|
363
|
+
##############################
|
364
|
+
# ELB
|
365
|
+
##############################
|
366
|
+
attach_to_elb(instance, elb_name) unless elb_name.nil?
|
367
|
+
|
368
|
+
##############################
|
369
|
+
# COMPLETED
|
370
|
+
##############################
|
371
|
+
puts ""
|
372
|
+
puts "Hostname : #{hostname}"
|
373
|
+
puts "Instance id: #{instance.id}"
|
374
|
+
puts "Public dns : #{instance.public_dns_name}"
|
375
|
+
puts "Private dns: #{instance.private_dns_name}"
|
376
|
+
puts "********************"
|
377
|
+
end
|
378
|
+
|
379
|
+
# Attaches an instance to the specified ELB.
|
380
|
+
#
|
381
|
+
# @param [AWS::EC2::Instance] instance newly created EC2 instance.
|
382
|
+
# @param [String] elb_name name of ELB.
|
383
|
+
#
|
384
|
+
def attach_to_elb(instance, elb_name)
|
385
|
+
begin
|
386
|
+
puts ""
|
387
|
+
puts "Adding to ELB: #{elb_name}"
|
388
|
+
elb = AWS::ELB.new
|
389
|
+
AWS.memoize do
|
390
|
+
# Build list of availability zones for any existing instances
|
391
|
+
zones = { }
|
392
|
+
zones[instance.availability_zone] = instance.availability_zone
|
393
|
+
elb.load_balancers[elb_name].instances.each do |elb_instance|
|
394
|
+
zones[elb_instance.availability_zone] = elb_instance.availability_zone
|
395
|
+
end
|
396
|
+
|
397
|
+
# Build list of existing zones
|
398
|
+
existing_zones = { }
|
399
|
+
elb.load_balancers[elb_name].availability_zones.each do |zone|
|
400
|
+
existing_zones[zone.name] = zone
|
401
|
+
end
|
402
|
+
|
403
|
+
# Enable zones
|
404
|
+
zones.keys.each do |zone_name|
|
405
|
+
elb.load_balancers[elb_name].availability_zones.enable(zones[zone_name])
|
406
|
+
end
|
407
|
+
|
408
|
+
# Disable zones
|
409
|
+
existing_zones.keys.each do |zone_name|
|
410
|
+
elb.load_balancers[elb_name].availability_zones.disable(existing_zones[zone_name]) unless zones.has_key?(zone_name)
|
411
|
+
end
|
412
|
+
|
413
|
+
elb.load_balancers[elb_name].instances.register(instance)
|
414
|
+
end
|
415
|
+
rescue StandardError => bang
|
416
|
+
puts "Error adding to load balancers: " + bang.to_s
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Given a list of possible directories, build a list of directories that actually exist.
|
421
|
+
#
|
422
|
+
# @param [Array<String>] directories list of possible directories
|
423
|
+
# @return [Array<String>] directories that exist or an empty array if none of the directories exist.
|
424
|
+
#
|
425
|
+
def build_list_of_valid_directories(directories)
|
426
|
+
dirs = []
|
427
|
+
unless directories.nil?
|
428
|
+
if directories.kind_of? Array
|
429
|
+
directories.each {|d| dirs << d if File.directory?(d) }
|
430
|
+
else
|
431
|
+
dirs << directories if File.directory?(directories)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
dirs
|
435
|
+
end
|
436
|
+
|
437
|
+
# Searches for the most recent AMI matching the criteria.
|
438
|
+
#
|
439
|
+
# @param [String] arch system archicture, `i386` or `x86_64`
|
440
|
+
# @param [String] virtualization virtualization type, `paravirtual` or `hvm`
|
441
|
+
# @param [Regex] ami_name_match regular expression that describes the AMI.
|
442
|
+
# @param [String, nil] id id of an AMI. If not nil, ami_name_match is ignored.
|
443
|
+
#
|
444
|
+
# @return [AmiDetails] AMI name and id.
|
445
|
+
def find_ami(arch, virtualization, ami_name_match, id = nil)
|
446
|
+
puts "Searching for AMI..."
|
447
|
+
ami_name = ""
|
448
|
+
ami_id = ""
|
449
|
+
|
450
|
+
# Retrieve list of AMIs
|
451
|
+
my_images = @ec2.images.with_owner('self')
|
452
|
+
|
453
|
+
if id.nil?
|
454
|
+
# Search for latest AMI with the right architecture and virtualization
|
455
|
+
my_images.each do |ami|
|
456
|
+
next if arch != ami.architecture.to_s
|
457
|
+
next if virtualization != ami.virtualization_type.to_s
|
458
|
+
next unless ami.state == :available
|
459
|
+
|
460
|
+
next if ! ami.name.match(ami_name_match)
|
461
|
+
|
462
|
+
if ami.name > ami_name
|
463
|
+
ami_name = ami.name
|
464
|
+
ami_id = ami.id
|
465
|
+
end
|
466
|
+
end
|
467
|
+
else
|
468
|
+
# Look for specified AMI
|
469
|
+
ami_arch = nil
|
470
|
+
my_images.each do |ami|
|
471
|
+
next if ami.id != id
|
472
|
+
ami_id = id
|
473
|
+
ami_name = ami.name
|
474
|
+
ami_arch = ami.architecture
|
475
|
+
end
|
476
|
+
|
477
|
+
# Check that AMI exists
|
478
|
+
if ami_id.nil?
|
479
|
+
abort("AMI id not found: #{ami_id}")
|
480
|
+
end
|
481
|
+
|
482
|
+
if arch != ami_arch.to_s
|
483
|
+
abort("Invalid AMI selection. Architecture for instance type (#{instance_type} - #{instance_architecture} - #{instance_virtualization}) does not match AMI arch (#{ami_arch.to_s}).")
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
AmiDetails.new(ami_name, ami_id)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Initializes connections to the AWS SDK
|
491
|
+
#
|
492
|
+
def initialize_aws()
|
493
|
+
aws_access_key = @options.access_key
|
494
|
+
aws_access_key ||= ENV['AWS_ACCESS_KEY']
|
495
|
+
|
496
|
+
aws_secret_access_key = @options.secret
|
497
|
+
aws_secret_access_key ||= ENV['AWS_SECRET_ACCESS_KEY']
|
498
|
+
|
499
|
+
if aws_access_key.nil? || aws_secret_access_key.nil?
|
500
|
+
abort("You MUST either set the AWS_ACCESS_KEY and AWS_SECRET_ACCESS_KEY environment variables or use the command line options.")
|
501
|
+
end
|
502
|
+
|
503
|
+
puts "Initializing AWS connection..."
|
504
|
+
AWS.config({
|
505
|
+
:access_key_id => aws_access_key,
|
506
|
+
:secret_access_key => aws_secret_access_key
|
507
|
+
})
|
508
|
+
end
|
509
|
+
|
510
|
+
# Generates a new hostname based on:
|
511
|
+
# * application base name
|
512
|
+
# * application name
|
513
|
+
# * application suffix
|
514
|
+
# * environment short name
|
515
|
+
# * environment name
|
516
|
+
#
|
517
|
+
def generate_hostname()
|
518
|
+
puts "Calculating host name..."
|
519
|
+
|
520
|
+
prefix = @application.basename
|
521
|
+
prefix ||= @application.name
|
522
|
+
|
523
|
+
env_suffix = @environment.short_name
|
524
|
+
env_suffix ||= @environment.name
|
525
|
+
|
526
|
+
suffix = env_suffix
|
527
|
+
unless @application.name_suffix.nil?
|
528
|
+
suffix = "#{@application.name_suffix}.#{env_suffix}"
|
529
|
+
end
|
530
|
+
|
531
|
+
regex = Regexp.new("#{prefix}(\\d+)[.]#{suffix.gsub(/[.]/, "[.]")}.*")
|
532
|
+
|
533
|
+
server_numbers = []
|
534
|
+
|
535
|
+
highest_server_number = 0
|
536
|
+
lowest_server_number = 32768
|
537
|
+
AWS.memoize do
|
538
|
+
server_instances = @ec2.instances.filter("tag:Name", "#{prefix}*#{suffix}*")
|
539
|
+
server_instances.each do |i|
|
540
|
+
next if i.status == :terminated
|
541
|
+
server_name = i.tags[:Name]
|
542
|
+
unless regex.match(server_name).nil?
|
543
|
+
server_num = $1.to_i
|
544
|
+
server_numbers << server_num
|
545
|
+
end
|
546
|
+
end
|
547
|
+
highest_server_number = server_numbers.max
|
548
|
+
end
|
549
|
+
|
550
|
+
# If the highest number server is less than 10, just add
|
551
|
+
# 1 to it. Otherwise, find the first available
|
552
|
+
# server number starting at 1.
|
553
|
+
host_number = 0
|
554
|
+
if highest_server_number.nil?
|
555
|
+
host_number = 1
|
556
|
+
elsif highest_server_number < 10
|
557
|
+
host_number = highest_server_number + 1
|
558
|
+
else
|
559
|
+
# Try to start over with 1 and find the
|
560
|
+
# first available host number
|
561
|
+
server_number_set = Set.new(server_numbers)
|
562
|
+
host_number = 1
|
563
|
+
while server_number_set.include?(host_number) do
|
564
|
+
host_number += 1
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
short_hostname = "#{prefix}#{host_number}.#{suffix}"
|
569
|
+
short_hostname
|
570
|
+
end
|
571
|
+
|
572
|
+
# Launches an EC2 instance.
|
573
|
+
#
|
574
|
+
# @param [String] FQDN for the new host.
|
575
|
+
# @param [String] ami_id id for the AMI to use.
|
576
|
+
# @param [String] availability_zone EC2 availability zone to use
|
577
|
+
# @param [String] key_name EC2 SSH key to use.
|
578
|
+
# @param [Array<String>] security_groups list of security groups.
|
579
|
+
# @param [String] instance_type EC2 instance type.
|
580
|
+
# @param [String] user_data command data to store pass to the instance in the EC2 user-data field.
|
581
|
+
# @param [Hash<String,Hash<String, String>, nil] block_device_mappings mapping of device names to block device details.
|
582
|
+
# See http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/EC2/InstanceCollection.html#create-instance_method.
|
583
|
+
# @param [Hash<String,Hash<String, String>>, nil] block_device_tags mapping of device names to hash objects with tags for the new EBS block devices.
|
584
|
+
#
|
585
|
+
# @return [AWS::EC2::Instance] newly created EC2 instance or nil if the launch failed.
|
586
|
+
def launch_instance(hostname, ami_id, availability_zone, key_name, security_groups, instance_type, user_data, block_device_mappings = nil, block_device_tags = nil, vpc_subnet = nil)
|
587
|
+
puts "Launching instance..."
|
588
|
+
new_instance = nil
|
589
|
+
run_with_backoff(30, 1, "launching instance") do
|
590
|
+
new_instance = @ec2.instances.create(
|
591
|
+
:image_id => ami_id,
|
592
|
+
:availability_zone => availability_zone,
|
593
|
+
:key_name => key_name,
|
594
|
+
:security_groups => security_groups,
|
595
|
+
:user_data => user_data,
|
596
|
+
:instance_type => instance_type,
|
597
|
+
:block_device_mappings => block_device_mappings,
|
598
|
+
:subnet => vpc_subnet
|
599
|
+
)
|
600
|
+
end
|
601
|
+
sleep 5
|
602
|
+
|
603
|
+
puts " Waiting for instance to start up..."
|
604
|
+
sleep 2
|
605
|
+
instance_ready = false
|
606
|
+
until instance_ready
|
607
|
+
sleep 1
|
608
|
+
begin
|
609
|
+
instance_ready = new_instance.status != :pending
|
610
|
+
rescue
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
unless new_instance.status == :running
|
615
|
+
puts "Instance launch failed. Aborting."
|
616
|
+
exit 5
|
617
|
+
end
|
618
|
+
|
619
|
+
##############################
|
620
|
+
# Tag instance
|
621
|
+
puts "Tagging instance..."
|
622
|
+
run_with_backoff(30, 1, "tag #{new_instance.id}, tag: name, value: #{hostname}") { new_instance.add_tag("Name", :value => hostname) }
|
623
|
+
run_with_backoff(30, 1, "tag #{new_instance.id}, tag: environment, value: #{@environment.name}") { new_instance.add_tag("environment", :value => @environment.name) }
|
624
|
+
run_with_backoff(30, 1, "tag #{new_instance.id}, tag: application, value: #{@application.name}") { new_instance.add_tag("application", :value => @application.name) }
|
625
|
+
|
626
|
+
##############################
|
627
|
+
# Tag volumes
|
628
|
+
unless block_device_tags.empty?
|
629
|
+
puts "Tagging volumes..."
|
630
|
+
AWS.start_memoizing
|
631
|
+
block_device_tags.keys.each do |device|
|
632
|
+
v = new_instance.block_device_mappings[device].volume
|
633
|
+
block_device_tags[device].keys.each do |tag_name|
|
634
|
+
run_with_backoff(30, 1, "tag #{v.id}, tag: #{tag_name}, value: #{block_device_tags[device][tag_name]}") do
|
635
|
+
v.add_tag(tag_name, :value => block_device_tags[device][tag_name])
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
AWS.stop_memoizing
|
640
|
+
end
|
641
|
+
|
642
|
+
new_instance
|
643
|
+
end
|
644
|
+
|
645
|
+
# Read in the configuration file stored in the workspace directory.
|
646
|
+
# By default this will be './config.rb'.
|
647
|
+
#
|
648
|
+
# @return [EC2Launcher::Config] the parsed configuration object
|
649
|
+
def load_config_file()
|
650
|
+
# Load configuration file
|
651
|
+
config_filename = File.join(@options.directory, "config.rb")
|
652
|
+
abort("Unable to find 'config.rb' in '#{@options.directory}'") unless File.exists?(config_filename)
|
653
|
+
ConfigDSL.execute(File.read(config_filename)).config
|
654
|
+
end
|
655
|
+
|
656
|
+
# Load and parse an environment file
|
657
|
+
#
|
658
|
+
# @param [String] name full pathname of the environment file to load
|
659
|
+
# @param [EC2Launcher::Environment, nil] default_environment the default environment,
|
660
|
+
# which will be used as the base for the new environment. Optional.
|
661
|
+
# @param [Boolean] fail_on_missing print an error and exit if the file does not exist.
|
662
|
+
#
|
663
|
+
# @return [EC2Launcher::Environment] the new environment loaded from the specified file.
|
664
|
+
#
|
665
|
+
def load_environment_file(name, default_environment = nil, fail_on_missing = false)
|
666
|
+
unless File.exists?(name)
|
667
|
+
abort("Unable to read environment: #{name}") if fail_on_missing
|
668
|
+
return nil
|
669
|
+
end
|
670
|
+
|
671
|
+
new_env = default_environment.clone unless default_environment.nil?
|
672
|
+
new_env ||= EC2Launcher::Environment.new
|
673
|
+
|
674
|
+
new_env.load(File.read(name))
|
675
|
+
new_env
|
676
|
+
end
|
677
|
+
|
678
|
+
# Attempts to build a list of valid directories.
|
679
|
+
#
|
680
|
+
# @param [Array<String>, nil] target_directories list of possible directories
|
681
|
+
# @param [String] default_directory directory to use if the target_directories list is empty or nil
|
682
|
+
# @param [String] name name of the type of directory. Used only for error messages.
|
683
|
+
# @param [Boolean] fail_on_error exit with an error if the list of valid directories is empty
|
684
|
+
#
|
685
|
+
# @return [Array<String] list of directories that exist
|
686
|
+
#
|
687
|
+
def process_directory_list(target_directories, default_directory, name, fail_on_error = false)
|
688
|
+
dirs = []
|
689
|
+
if target_directories.nil?
|
690
|
+
dirs << File.join(@options.directory, default_directory)
|
691
|
+
else
|
692
|
+
target_directories.each {|d| dirs << File.join(@options.directory, d) }
|
693
|
+
end
|
694
|
+
valid_directories = build_list_of_valid_directories(dirs)
|
695
|
+
|
696
|
+
if valid_directories.empty?
|
697
|
+
temp_dirs = dirs.each {|d| "'#{d}'"}.join(", ")
|
698
|
+
if fail_on_error
|
699
|
+
abort("ERROR - #{name} directories not found: #{temp_dirs}")
|
700
|
+
else
|
701
|
+
puts "WARNING - #{name} directories not found: #{temp_dirs}"
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
valid_directories
|
706
|
+
end
|
707
|
+
|
708
|
+
# Validates all settings in an application file
|
709
|
+
#
|
710
|
+
# @param [String] filename name of the application file
|
711
|
+
# @param [EC2Launcher::Application] application application object to validate
|
712
|
+
#
|
713
|
+
def validate_application(filename, application)
|
714
|
+
unless application.availability_zone.nil? || AVAILABILITY_ZONES.include?(application.availability_zone)
|
715
|
+
abort("Invalid availability zone '#{application.availability_zone}' in application '#{application.name}' (#{filename})")
|
716
|
+
end
|
717
|
+
|
718
|
+
unless application.instance_type.nil? || INSTANCE_TYPES.include?(application.instance_type)
|
719
|
+
abort("Invalid instance type '#{application.instance_type}' in application '#{application.name}' (#{filename})")
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
# Validates all settings in an environment file
|
724
|
+
#
|
725
|
+
# @param [String] filename name of the environment file
|
726
|
+
# @param [EC2Launcher::Environment] environment environment object to validate
|
727
|
+
#
|
728
|
+
def validate_environment(filename, environment)
|
729
|
+
unless environment.availability_zone.nil? || AVAILABILITY_ZONES.include?(environment.availability_zone)
|
730
|
+
abort("Invalid availability zone '#{environment.availability_zone}' in environment '#{environment.name}' (#{filename})")
|
731
|
+
end
|
732
|
+
end
|
733
|
+
end
|
734
|
+
end
|