ec2launcher 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,270 @@
1
+ #
2
+ # Copyright (c) 2012 Sean Laurent
3
+ #
4
+ require 'ec2launcher/defaults'
5
+
6
+ module EC2Launcher
7
+ # Helper class to build EC2 block device definitions.
8
+ #
9
+ class BlockDeviceBuilder
10
+ attr_reader :block_device_setup
11
+ attr_reader :block_device_mappings
12
+ attr_reader :block_device_tags
13
+
14
+ # @param [AWS::EC2] ec2 interface to ec2
15
+ # @param [Integer, nil] volume_size size of new EBS volumes. If set to nil, uses EC2Launcher::Defaults::DEFAULT_VOLUME_SIZE.
16
+ #
17
+ def initialize(ec2, volume_size = nil)
18
+ @ec2 = ec2
19
+ @block_size = volume_size
20
+ @volume_size ||= EC2Launcher::DEFAULT_VOLUME_SIZE
21
+
22
+ @block_device_setup = {}
23
+ @block_device_mappings = {}
24
+ @block_device_tags = {}
25
+ end
26
+
27
+ # Iterates over a number of block_devices, executing the specified Ruby block.
28
+ #
29
+ # @param [Integer] count number of block devices
30
+ # @param [String, "sdf"] device the starting device name. Defaults to "sdf".
31
+ # Incremented for each iteration.
32
+ # @param [Block] block block to execute. Passes in the current device name and zero-based index.
33
+ #
34
+ def build_block_devices(count, device = "sdf", &block)
35
+ device_name = device
36
+ 0.upto(count - 1).each do |index|
37
+ yield device_name, index
38
+ device_name.next!
39
+ end
40
+ end
41
+
42
+ # Creates the mappings for the appropriate EBS volumes.
43
+ #
44
+ # @param [String] hostname FQDN for new host
45
+ # @param [String] short_hostname short name for host host, without domain name.
46
+ # @param [String] environment_name name of the environment
47
+ # @param [Array<EC2Launcher::BlockDevice>] block_devices list of block devices to create.
48
+ #
49
+ def build_ebs_volumes(hostname, short_hostname, environment_name, block_devices)
50
+ base_device_name = "sdf"
51
+ block_devices.each do |block_device|
52
+ build_block_devices(block_device.count, base_device_name) do |device_name, index|
53
+ volume_size = block_device.size
54
+ volume_size ||= @volume_size
55
+
56
+ block_device_mappings["/dev/#{device_name}"] = {
57
+ :volume_size => volume_size,
58
+ :delete_on_termination => true
59
+ }
60
+
61
+ block_device_tags["/dev/#{device_name}"] = {
62
+ "purpose" => block_device.name,
63
+ "host" => hostname,
64
+ "environment" => environment_name
65
+ }
66
+
67
+ if block_device.raid_level.nil?
68
+ block_device_tags["/dev/#{device_name}"]["Name"] = "#{short_hostname} #{block_device.name}"
69
+ else
70
+ block_device_tags["/dev/#{device_name}"]["Name"] = "#{short_hostname} #{block_device.name} raid (#{(index + 1).to_s})"
71
+ block_device_tags["/dev/#{device_name}"]["raid_number"] = (index + 1).to_s
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # Creates the mappings for the appropriate ephemeral drives.
78
+ #
79
+ # @param [String] instance_type type of instance. See EC2Launcher::Defaults::INSTANCE_TYPES.
80
+ #
81
+ def build_ephemeral_drives(instance_type)
82
+ ephemeral_drive_count = case instance_type
83
+ when "m1.small" then 1
84
+ when "m1.medium" then 1
85
+ when "m2.xlarge" then 1
86
+ when "m2.2xlarge" then 1
87
+ when "c1.medium" then 1
88
+ when "m1.large" then 2
89
+ when "m2.4xlarge" then 2
90
+ when "cc1.4xlarge" then 2
91
+ when "cg1.4xlarge" then 2
92
+ when "m1.xlarge" then 4
93
+ when "c1.xlarge" then 4
94
+ when "cc2.8xlarge" then 4
95
+ else 0
96
+ end
97
+ build_block_devices(ephemeral_drive_count, "sdb") do |device_name, index|
98
+ @block_device_mappings["/dev/#{device_name}"] = "ephemeral#{index}"
99
+ end
100
+ end
101
+
102
+ # Finds the EBS snapshots to clone for all appropriate block devices and
103
+ # updates the block device mapping hash.
104
+ #
105
+ # @param [EC2Launcher::Environment] environment current environment
106
+ # @param [EC2Launcher::Application] application current application
107
+ # @param [String] clone_host name of host to clone
108
+ #
109
+ def clone_volumes(environment, application, clone_host)
110
+ return if clone_host.nil?
111
+
112
+ puts "Retrieving snapshots..."
113
+ AWS.start_memoizing
114
+ base_device_name = "sdf"
115
+ application.block_devices.each do |block_device|
116
+ if block_device.raid_level.nil?
117
+ @block_device_mappings["/dev/#{base_device_name}"][:snapshot_id] = get_latest_snapshot(clone_short_hostname, clone_host, block_device.name).id
118
+ base_device_name.next!
119
+ else
120
+ snapshots = get_latest_raid_snapshot_mapping(clone_host, block_device.name, block_device.count)
121
+ build_block_devices(block_device.count, base_device_name) do |device_name, index|
122
+ @block_device_mappings["/dev/#{device_name}"][:snapshot_id] = snapshots[(index + 1).to_s].id
123
+ end
124
+ end
125
+ end
126
+ AWS.stop_memoizing
127
+ end
128
+
129
+ # Generates the mappings for ephemeral and ebs volumes.
130
+ #
131
+ # @param [String] hostname FQDN for new host
132
+ # @param [String] short_hostname short name for host host, without domain name.
133
+ # @param [String] instance_type type of instance. See EC2Launcher::Defaults::INSTANCE_TYPES.
134
+ # @param [EC2Launcher::Environment] environment current environment
135
+ # @param [EC2Launcher::Application] application current application
136
+ # @param [String, nil] clone_host FQDN of host to clone or nill to skip cloning.
137
+ #
138
+ def generate_block_devices(hostname, short_hostname, instance_type, environment, application, clone_host = nil)
139
+ build_ephemeral_drives(instance_type)
140
+ build_ebs_volumes(hostname, short_hostname, environment.name, application.block_devices)
141
+ clone_volumes(environment, application, clone_host)
142
+ end
143
+
144
+ # Retrieves the latest set of completed snapshots for a RAID array of EBS volumes.
145
+ #
146
+ # Volumes must have the following tags:
147
+ # * host
148
+ # * volumeName
149
+ # * time
150
+ #
151
+ # @param [String] hostname FQDN for new host
152
+ # @param [String] purpose purpose of RAID array, which is stored in the `purpose` tag for snapshots/volumes
153
+ # and is part of the snapshot name.
154
+ # @param [Integer] count number of EBS snapshots to look for
155
+ #
156
+ # @return [Hash<Integer, AWS::EC2::Snapshot>] mapping of RAID device numbers (zero based) to AWS Snapshots.
157
+ #
158
+ def get_latest_raid_snapshot_mapping(hostname, purpose, count)
159
+ puts "Retrieving list of snapshots ..."
160
+ result = @ec2.snapshots.tagged("host").tagged_values(hostname).tagged("volumeName").tagged_values("*raid*").tagged("time")
161
+
162
+ puts "Building list of snapshots to clone (#{purpose}) for '#{hostname}'..."
163
+ snapshot_name_regex = /#{purpose} raid.*/
164
+ host_snapshots = []
165
+ result.each do |s|
166
+ next if snapshot_name_regex.match(s.tags["volumeName"]).nil?
167
+ host_snapshots << s if s.status == :completed
168
+ end
169
+
170
+ # Bucket the snapshots based on volume number e.g. "raid (1)" vs "raid (2)"
171
+ snapshot_buckets = { }
172
+ volume_number_regex = /raid \((\d)\)$/
173
+ host_snapshots.each do |snapshot|
174
+ next if snapshot.tags["time"].nil?
175
+
176
+ volume_name = snapshot.tags["volumeName"]
177
+
178
+ match_info = volume_number_regex.match(volume_name)
179
+ next if match_info.nil?
180
+
181
+ matches = match_info.captures
182
+ next if matches.length != 1
183
+
184
+ raid_number = matches[0]
185
+
186
+ snapshot_buckets[raid_number] = [] if snapshot_buckets[raid_number].nil?
187
+ snapshot_buckets[raid_number] << snapshot
188
+ end
189
+
190
+ # Sort the snapshots in each bucket by time
191
+ snapshot_buckets.keys.each do |key|
192
+ snapshot_buckets[key].sort! do |a, b|
193
+ b.tags["time"] <=> a.tags["time"]
194
+ end
195
+ end
196
+
197
+ # We need to find the most recent backup time that all snapshots have in common.
198
+ #
199
+ # For example, we may have the following snapshots for "raid (1)":
200
+ # volumeName => db1.dev db raid (1), time => 11-06-15 10:00
201
+ # volumeName => db1.dev db raid (1), time => 11-06-15 09:00
202
+ # volumeName => db1.dev db raid (1), time => 11-06-14 09:00
203
+ # And the following snapshots for "raid (2)":
204
+ # volumeName => db1.dev db raid (1), time => 11-06-15 09:00
205
+ # volumeName => db1.dev db raid (1), time => 11-06-14 09:00
206
+ #
207
+ # In this example, the latest snapshot from "raid (1)" is dated "11-06-15 10:00", but "raid (2)" does not have
208
+ # a matching snapshot (because it hasn't completed yet). Instead, we should use the "11-06-15 09:00" snapshots.
209
+ #
210
+ # We find the most recent date from each bucket and then take the earliest one.
211
+ most_recent_dates = []
212
+ snapshot_buckets.keys().each do |key|
213
+ snapshot = snapshot_buckets[key][0]
214
+ most_recent_dates << snapshot.tags["time"].to_s
215
+ end
216
+ most_recent_dates.sort!
217
+
218
+ puts "Most recent snapshot: #{most_recent_dates[0]}"
219
+
220
+ snapshot_mapping = { }
221
+ AWS.memoize do
222
+ snapshot_buckets.keys.each do |index|
223
+ found = false
224
+ snapshot_buckets[index].each do |snapshot|
225
+ if snapshot.tags["time"] == most_recent_dates[0]
226
+ snapshot_mapping[index] = snapshot
227
+ found = true
228
+ break
229
+ end
230
+ end
231
+
232
+ abort("***** ERROR: Unable to find snapshot for #{purpose} (#{index.to_s})") unless found
233
+ end
234
+ end
235
+ snapshot_mapping
236
+ end
237
+
238
+ # Retrieves the most recent snapshot with a specific name AND one or more other filters
239
+ #
240
+ # @param [String] server_name name of server with the volume to clone.
241
+ # @param [String, nil] extra additional AWS compatible filters for searching.
242
+ #
243
+ # @return [AWS::EC2::Snapshot, nil] matching snapshot or nil if no matching snapshot
244
+ #
245
+ def get_latest_snapshot(server_name, extra = nil)
246
+ puts " Retrieving snapshot for #{server_name} #{extra}"
247
+
248
+ filter = server_name
249
+ filter += " (#{server_name} #{extra})" unless extra.nil? || extra.empty?
250
+ get_latest_snapshot_by_name(filter)
251
+ end
252
+
253
+ # Retrieves the most recent snapshot for a volume with a specific name
254
+ #
255
+ # @param [String] name_filter AWS compatible filter for the Name tag on a snapshot
256
+ #
257
+ # @return [AWS::EC2::Snapshot, nil] matching snapshot or nil if no matching snapshot
258
+ #
259
+ def get_latest_snapshot_by_name(name_filter)
260
+ puts " Retrieving snapshot with filter tag:Name='#{name_filter}'"
261
+
262
+ snapshot = nil
263
+ @ec2.snapshots.filter("tag:Name", name_filter).each do |s|
264
+ next unless s.status == :completed
265
+ snapshot = s if snapshot.nil? || snapshot.start_time < s.start_time
266
+ end
267
+ snapshot
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,76 @@
1
+ #
2
+ # Copyright (c) 2012 Sean Laurent
3
+ #
4
+ module EC2Launcher
5
+ class ConfigDSL
6
+ attr_reader :config
7
+
8
+ def config(&block)
9
+ return @config if block.nil?
10
+
11
+ @config = Config.new
12
+ @config.instance_eval &block
13
+ @config
14
+ end
15
+
16
+ def self.execute(dsl)
17
+ new.tap do |context|
18
+ context.instance_eval(dsl)
19
+ end
20
+ end
21
+ end
22
+
23
+ class Config
24
+ DEFAULT_CONFIG_ERB = %q{
25
+ config do
26
+ environments "environments"
27
+ applications "applications"
28
+
29
+ package_manager "apt"
30
+ config_manager "chef"
31
+ end
32
+ }.gsub(/^ /, '')
33
+
34
+ def environments(*environments)
35
+ if environments.empty?
36
+ @environments
37
+ else
38
+ if environments[0].kind_of? Array
39
+ @environments = @environments[0]
40
+ else
41
+ @environments = [ environments[0] ]
42
+ end
43
+ self
44
+ end
45
+ end
46
+
47
+ def applications(*applications)
48
+ if applications.empty?
49
+ @applications
50
+ else
51
+ if applications[0].kind_of? Array
52
+ @applications = @applications[0]
53
+ else
54
+ @applications = [ applications[0] ]
55
+ end
56
+ self
57
+ end
58
+ end
59
+
60
+ def package_manager(*package_manager)
61
+ if package_manager.empty?
62
+ @package_manager
63
+ else
64
+ @package_manager = package_manager[0]
65
+ end
66
+ end
67
+
68
+ def config_manager(*config_manager)
69
+ if config_manager.empty?
70
+ @config_manager
71
+ else
72
+ @config_manager = config_manager[0]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,9 @@
1
+ #
2
+ # Copyright (c) 2012 Sean Laurent
3
+ #
4
+ module EC2Launcher
5
+ DEFAULT_VOLUME_SIZE = 60 # in GB
6
+
7
+ AVAILABILITY_ZONES = %w{us-east-1a us-east-1b us-east-1c us-east-1d}
8
+ INSTANCE_TYPES = %w{m1.small m1.medium m1.large m1.xlarge t1.micro m2.xlarge m2.2xlarge m2.4xlarge c1.medium c1.xlarge cc1.4xlarge cg1.4xlarge}
9
+ end
@@ -0,0 +1,62 @@
1
+ #
2
+ # Copyright (c) 2012 Sean Laurent
3
+ #
4
+ module EmailNotifications
5
+ attr_reader :email_notifications
6
+
7
+ def email_notification(&block)
8
+ notifications = EmailNotification.new
9
+ notifications.instance_exec(&block)
10
+ @email_notifications = notifications
11
+ end
12
+ end
13
+
14
+ class EmailNotification
15
+ def initialize()
16
+ end
17
+
18
+ def from(*from)
19
+ if from.empty?
20
+ @from
21
+ else
22
+ @from = from[0]
23
+ self
24
+ end
25
+ end
26
+
27
+ def to(*to)
28
+ if to.empty?
29
+ @to
30
+ else
31
+ @to = to[0]
32
+ self
33
+ end
34
+ end
35
+
36
+ def ses_access_key(*ses_access_key)
37
+ if ses_access_key.empty?
38
+ @ses_access_key
39
+ else
40
+ @ses_access_key = ses_access_key[0]
41
+ self
42
+ end
43
+ end
44
+
45
+ def ses_secret_key(*ses_secret_key)
46
+ if ses_secret_key.empty?
47
+ @ses_secret_key
48
+ else
49
+ @ses_secret_key = ses_secret_key[0]
50
+ self
51
+ end
52
+ end
53
+
54
+ def to_json(*a)
55
+ {
56
+ "from" => @from,
57
+ "to" => @to,
58
+ "ses_access_key" => @ses_access_key,
59
+ "ses_secret_key" => @ses_secret_key
60
+ }.to_json(*a)
61
+ end
62
+ end
@@ -0,0 +1,179 @@
1
+ #
2
+ # Copyright (c) 2012 Sean Laurent
3
+ #
4
+ require 'ec2launcher/email_notification'
5
+
6
+ module EC2Launcher
7
+ class Environment
8
+ include EmailNotifications
9
+
10
+ attr_reader :name
11
+
12
+ def initialize()
13
+ @aliases = []
14
+ @email_notifications = nil
15
+ @gems = []
16
+ @packages = []
17
+ end
18
+
19
+ def environment(name)
20
+ @name = name
21
+ yield self
22
+ self
23
+ end
24
+
25
+ def aws_keyfile(*aws_keyfile)
26
+ if aws_keyfile.empty?
27
+ @aws_keyfile
28
+ else
29
+ @aws_keyfile = aws_keyfile[0]
30
+ self
31
+ end
32
+ end
33
+
34
+ def aliases(*aliases)
35
+ if aliases.empty?
36
+ @aliases
37
+ else
38
+ if aliases[0].kind_of? String
39
+ @aliases = [ aliases[0] ]
40
+ else
41
+ @aliases = aliases[0]
42
+ end
43
+ end
44
+ end
45
+
46
+ def ami_name(*ami_name)
47
+ if ami_name.empty?
48
+ @ami_name
49
+ else
50
+ if ami_name[0].kind_of? String
51
+ @ami_name = /#{ami_name[0]}/
52
+ else
53
+ @ami_name = ami_name[0]
54
+ end
55
+ self
56
+ end
57
+ end
58
+
59
+ def availability_zone(*zone)
60
+ if zone.empty?
61
+ @availability_zone
62
+ else
63
+ @availability_zone = zone[0].to_s
64
+ self
65
+ end
66
+ end
67
+
68
+ def chef_server_url(*server_url)
69
+ if server_url.empty?
70
+ @chef_server_url
71
+ else
72
+ @chef_server_url = server_url[0]
73
+ self
74
+ end
75
+ end
76
+
77
+ def chef_validation_pem_url(*chef_validation_pem_url)
78
+ if chef_validation_pem_url.empty?
79
+ @chef_validation_pem_url
80
+ else
81
+ @chef_validation_pem_url = chef_validation_pem_url[0]
82
+ self
83
+ end
84
+ end
85
+
86
+ def domain_name(*domain_name)
87
+ if domain_name.empty?
88
+ @domain_name
89
+ else
90
+ @domain_name = domain_name[0]
91
+ self
92
+ end
93
+ end
94
+
95
+ def gems(*gems)
96
+ if gems.empty?
97
+ @gems
98
+ else
99
+ @gems = gems[0]
100
+ self
101
+ end
102
+ end
103
+
104
+ def key_name(*key)
105
+ if key.empty?
106
+ @key_name
107
+ else
108
+ @key_name = key[0]
109
+ self
110
+ end
111
+ end
112
+
113
+ def packages(*packages)
114
+ if packages.empty?
115
+ @packages
116
+ else
117
+ @packages = packages[0]
118
+ self
119
+ end
120
+ end
121
+
122
+ def roles(*roles)
123
+ if roles.empty?
124
+ @roles
125
+ else
126
+ @roles = [] if @roles.nil?
127
+ if roles[0].kind_of? Array
128
+ @roles += roles[0]
129
+ else
130
+ @roles = []
131
+ @roles << roles[0]
132
+ end
133
+ self
134
+ end
135
+ end
136
+
137
+ def security_groups(*security_groups)
138
+ if security_groups.empty?
139
+ @security_groups
140
+ else
141
+ @security_groups = [] if @security_groups.nil?
142
+ if security_groups[0].kind_of? Array
143
+ @security_groups += security_groups[0]
144
+ else
145
+ @security_groups << security_groups[0]
146
+ end
147
+ self
148
+ end
149
+ end
150
+
151
+ def short_name(*short_name)
152
+ if short_name.empty?
153
+ @short_name
154
+ else
155
+ @short_name = short_name[0]
156
+ self
157
+ end
158
+ end
159
+
160
+ def subnet(*subnet)
161
+ if subnet.empty?
162
+ @subnet
163
+ else
164
+ @subnet = subnet[0]
165
+ self
166
+ end
167
+ end
168
+
169
+ def load(dsl)
170
+ self.instance_eval(dsl)
171
+ self
172
+ end
173
+
174
+ def self.load(dsl)
175
+ env = Environment.new.instance_eval(dsl)
176
+ env
177
+ end
178
+ end
179
+ end