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