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.
@@ -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