-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsplitAMI.rb
executable file
·292 lines (261 loc) · 10.9 KB
/
splitAMI.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env ruby
# creates an AMI that spans multiple EBS snapshots, based on an input AMI
# Usage:
# splitAMI.rb [SOURCE_AMI] [PATH]:[SIZE]:[MOUNT_OPTIONS] [[PATH]:[SIZE]...]
require 'aws-sdk'
require 'aws-sdk-resources'
require 'fileutils'
require 'logger'
require 'open-uri'
require 'time'
################################
# STAGE 0 - CONFIGURATION
EBS_ROOT_DEVICES = ['sda1', 'xvda']
ROOT_DEV_PARTITION = ENV['ROOT_DEV_PARTITION'] || 1
WORKDIR = '/newami'
REMOVE_TAGS = ['Name', 'Description', 'CreatedAt', 'CreatedFrom']
TIMESTAMP = Time.now.strftime("%Y/%m/%d at %H:%M:%S")
ILLEGAL_CHARS = /[^a-zA-z0-9().,-\/_ ]+/ # characters illegal in AMI names
PUBLICIMAGE = ENV['PUBLICIMAGE'] || 'false'
src_id, *fs_params = ARGV
log = Logger.new(STDOUT)
log.formatter = proc do |severity, datetime, progname, msg|
"#{severity}: #{msg}\n"
end
# validate arguments
unless src_id =~ /\Aami-[0-9a-f]+\Z/
abort "Initial argument (#{src_id}) must be an EC2 AMI ID in the current region"
end
abort "Filesystem parameters (PATH:SIZE:FS_OPTIONS) required" unless fs_params.count > 0
FS_PARAM_MATCH = %r{\A(/[^:]+):([0-9.]+):([^:]+)\Z}
fs_params.each do |fs_param|
unless fs_param =~ FS_PARAM_MATCH
abort "Filesystem parameters must match #{FS_PARAM_MATCH.to_s}"
end
end
log.info "Detecting AWS Region from EC2 metadata service..."
METADATA_URL = 'http://169.254.169.254/latest/meta-data/'
AZ = open("#{METADATA_URL}/placement/availability-zone"){|io| io.read}
REGION = AZ.chop
client = Aws::EC2::Client.new(region: REGION)
my_instance_id = open("#{METADATA_URL}/instance-id"){|io| io.read}
Aws.config.update(region: REGION)
# assign an unused local unix device name to each of the fs_params
candidate_devices = ('b'...'z').to_a.map{|dev| "/dev/xvd#{dev}"}
available_devices = (candidate_devices - Dir["/dev/xvd*"]).sort
local_device_map = {'root' => available_devices.first}
i = 0 ; while i < fs_params.count do
local_device_map[fs_params[i]] = available_devices[i+1]
i += 1
end
################################
# STAGE 1 - Mount a new volume with the contents of the source AMI's root disk
src_ami = Aws::EC2::Image.new(src_id)
ami_name = "#{src_ami.name} split on #{TIMESTAMP}".gsub(ILLEGAL_CHARS,'-')
src_mappings = src_ami.block_device_mappings
base_tags = src_ami.tags.delete_if {|t| REMOVE_TAGS.include? t.key} <<
Aws::EC2::Types::Tag.new(key: "CreatedAt", value: TIMESTAMP) <<
Aws::EC2::Types::Tag.new(key: "CreatedFrom", value: src_id)
# find the original root disk's snapshot ID
root_mapping = src_mappings.find do |m|
EBS_ROOT_DEVICES.find {|dev| m.device_name =~ /#{dev}\Z/}
end
if root_mapping == nil
abort "Can't determine root volume from #{src_mappings}"
end
if root_mapping.ebs == nil
abort "Source AMI root volume #{root_mapping} must be EBS-based"
end
root_snapshot = root_mapping.ebs.snapshot_id
# assign an unused final unix device name to each of the fs_params
candidate_letters = ('b'...'z').to_a
used_letters = src_mappings.map{|m| m.device_name.gsub(/\d+\Z/,'')[-1, 1]}
available_letters = (candidate_letters - used_letters).sort
final_device_map = {}
i = 0 ; while i < fs_params.count do
final_device_map[fs_params[i]] = available_letters[i+1]
i += 1
end
# create a new volume from the original root disk's snapshot ID
log.info "Creating a new root volume from snapshot ID #{root_snapshot}..."
root_volume_id = client.create_volume({
availability_zone: AZ,
size: root_mapping.ebs.volume_size,
volume_type: root_mapping.ebs.volume_type,
snapshot_id: root_snapshot,
}).volume_id
log.info "Waiting until root volume (#{root_volume_id}) is available..."
client.wait_until(:volume_available, volume_ids: [root_volume_id])
# attach the volume and mount it at WORKDIR/root
root_volume_path = "#{WORKDIR}/root"
root_volume_device = "#{local_device_map['root']}#{ROOT_DEV_PARTITION}"
log.info "Attaching root volume at #{local_device_map['root']}..."
FileUtils.mkdir_p(root_volume_path)
resp = client.attach_volume({
device: local_device_map['root'],
instance_id: my_instance_id,
volume_id: root_volume_id,
})
client.wait_until(:volume_in_use, volume_ids: [root_volume_id])
sleep 30 # volume is still not attached sometimes?
log.info "Mounting root device #{root_volume_device} at #{root_volume_path}..."
`mount #{root_volume_device} #{root_volume_path}`
################################
# STAGE 2 - Create a new EBS Snapshot for each of the fs_params
mappings = src_mappings.map{|m| m.to_h} # track the created Device Mappings
snapshot_ids = [] # also track created snapshot IDs
# iterate over each desired filesystem parameters,
# It's important to handle subdirectories before parent directories, so fs_params
# must be lexically sorted by the path, and then reversed
fs_params.sort{|a,b| a.split(':')[0] <=> b.split(':')[0]}.reverse.each do |fs_param|
path, size, opts = fs_param.split(':')
# create a new EBS data volume and wait until it's ready
log.info "Creating a new #{size}GB data volume for #{path}..."
data_volume_id = client.create_volume({
availability_zone: AZ,
size: size,
volume_type: 'gp2',
}).volume_id
log.info "Waiting until volume for #{path} (#{data_volume_id}) is available..."
client.wait_until(:volume_available, volume_ids: [data_volume_id])
# attach the data volume
data_volume_device = local_device_map[fs_param]
data_volume_path = "/newami/#{data_volume_device.split('/').last}"
log.info "Attaching data volume for #{path} at #{data_volume_device}..."
FileUtils.mkdir_p(data_volume_path)
resp = client.attach_volume({
device: data_volume_device,
instance_id: my_instance_id,
volume_id: data_volume_id,
})
client.wait_until(:volume_in_use, volume_ids: [data_volume_id])
sleep 30 # volume is still not attached sometimes?
log.info "Creating filesystem for #{path} on #{data_volume_device}..."
`mkfs -t ext4 #{data_volume_device}`
log.info "Mounting data volume for #{path} at #{data_volume_path}..."
`mount #{data_volume_device} #{data_volume_path}`
# move the data from /newami/root/[PATH] to /newami/[PATH]
src_dir = root_volume_path + path
FileUtils.mkdir_p src_dir # create source directory as needed
log.info "Copying data from #{src_dir} -> #{data_volume_path}..."
`tar -C #{src_dir} -cf - . | tar -C #{data_volume_path} -xBf -`
log.info "Deleting data from #{src_dir}..."
`rm -rf #{src_dir}/* #{src_dir}/.* >/dev/null 2>&1`
log.info "Writing fstab entry for #{path} into #{root_volume_path}/etc/fstab"
device = "xvd#{final_device_map[fs_param]}"
File.open("#{root_volume_path}/etc/fstab", 'a') do |fstab|
fstab.write "/dev/#{device}\t#{path}\text4\t#{opts}\t0\t0\n"
end
# unmount, detach, snapshot, and delete the volume
log.info "Unmounting data volume for #{path} at #{data_volume_path}..."
`umount #{data_volume_path}`
log.info "Detaching data volume for #{path} (#{data_volume_device})..."
client.detach_volume(volume_id: data_volume_id)
client.wait_until(:volume_available, volume_ids: [data_volume_id])
log.info "Snapshotting data volume for #{path} #{data_volume_id}..."
snap_name = "#{ami_name} - #{path}"
snap_description = "#{ami_name} #{device}, mounted at #{path}"
snapshot_id = client.create_snapshot(
description: snap_description,
volume_id: data_volume_id,
).snapshot_id
log.info "Temporarily tagging snapshot #{snapshot_id}..."
client.create_tags(
resources: [snapshot_id],
tags: base_tags <<
Aws::EC2::Types::Tag.new(key: "Name", value: snap_name) <<
Aws::EC2::Types::Tag.new(key: "Description", value: snap_description))
snapshot_ids << snapshot_id
log.info "Deleting data volume for #{path} #{data_volume_id}..."
client.delete_volume(volume_id: data_volume_id)
# create the AWS Block Device Mapping for this volume
mappings << {
device_name: device,
virtual_name: "ebs",
ebs: {
snapshot_id: snapshot_id,
delete_on_termination: true,
volume_type: 'gp2',
}
}
end # Done with fs_params
################################
# STAGE 3 - Snapshot and delete the root volume
log.info "Unmounting root volume from #{root_volume_path}..."
`umount #{root_volume_path} && rm -rf #{WORKDIR}` # clean up
log.info "Detaching root volume #{root_volume_id}..."
client.detach_volume(volume_id: root_volume_id)
client.wait_until(:volume_available, volume_ids: [root_volume_id])
log.info "Snapshotting root volume #{root_volume_id}..."
new_root_snapshot_id = client.create_snapshot({
description: "#{src_ami.name} root disk",
volume_id: root_volume_id,
}).snapshot_id
snapshot_ids << new_root_snapshot_id
log.info "Deleting root volume #{root_volume_id}..."
client.delete_volume(volume_id: root_volume_id)
# create the final AWS Block Device Mappings
mappings.each do |mapping|
if mapping[:ebs]
mapping[:ebs].delete(:encrypted)
if mapping[:device_name] == root_mapping.device_name
mapping[:ebs][:snapshot_id] = new_root_snapshot_id
end
end
end
log.debug "Final block device mappings: #{mappings}"
log.info "Waiting for snapshots to complete..."
client.wait_until(:snapshot_completed, snapshot_ids: snapshot_ids)
################################
# STAGE 4 - Register the new AMI and tag created resources
# first, make all snapshots public
if PUBLICIMAGE != 'false'
snapshot_ids.each do |snap_id|
client.modify_snapshot_attribute(
attribute: "createVolumePermission",
group_names: ["all"],
operation_type: "add",
snapshot_id: snap_id)
end
end
log.info "Registering new AMI Image #{ami_name}..."
new_ami_id = client.register_image({
name: ami_name,
description: "#{src_ami.description} - split",
architecture: src_ami.architecture,
kernel_id: src_ami.kernel_id,
ramdisk_id: src_ami.ramdisk_id,
root_device_name: root_mapping.device_name,
block_device_mappings: mappings,
virtualization_type: src_ami.virtualization_type,
sriov_net_support: src_ami.sriov_net_support,
ena_support: src_ami.ena_support,
}).image_id
log.info "Waiting for AMI #{new_ami_id} to become ready..."
client.wait_until(:image_available, image_ids: [new_ami_id])
log.info "Tagging AMI #{new_ami_id}..."
client.create_tags(
resources: [new_ami_id],
tags: base_tags <<
Aws::EC2::Types::Tag.new(key: "Name", value: ami_name) <<
Aws::EC2::Types::Tag.new(key: "Description", value:
"#{src_ami.description} split on #{TIMESTAMP}"))
# tag all created snapshots
mappings.each do |mapping|
if mapping[:ebs] && snapshot_ids.include?(mapping[:ebs][:snapshot_id])
log.info "Final tagging snapshot #{mapping[:ebs][:snapshot_id]}..."
client.create_tags(
resources: [mapping[:ebs][:snapshot_id]],
tags: base_tags <<
Aws::EC2::Types::Tag.new(key: "Name", value:
"#{new_ami_id} #{mapping[:device_name]}"))
end
end
if PUBLICIMAGE != 'false'
log.info "Making AMI #{new_ami_id} public..."
client.modify_image_attribute({
image_id: new_ami_id,
launch_permission: {add: [{group: "all"}]}})
end
################################
log.info "Done. Created AMI #{new_ami_id} => #{ami_name}"