1
# Copyright 2008 Amazon.com, Inc. or its affiliates. All Rights
2
# Reserved. Licensed under the Amazon Software License (the
3
# "License"). You may not use this file except in compliance with the
4
# License. A copy of the License is located at
5
# http://aws.amazon.com/asl or in the "license" file accompanying this
6
# file. This file is distributed on an "AS IS" BASIS, WITHOUT
7
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
8
# the License for the specific language governing permissions and
9
# limitations under the License.
11
#---------------------------------------------------------------------#
12
# Create a Solaris EC2 Image as follows:
13
# - create a bootable file-system archive in the SUN flash format
14
# - create, format and mount a blank file-system image
15
# - replicate the archive section of the flash archive into the image
16
# - customize the image
18
# Fasten your seat-belts and grab a pillow; this is painfully slow.
19
# Initial tests show an average bundling time of a virgin OpenSolaris
20
# system using this algorithm to be about 85-90 minutes. Optimization
21
# involves rewriting flar to combine the "flar create" and "flar split"
22
# steps into a "flar replicate" step
23
#---------------------------------------------------------------------#
26
require 'ec2/oem/open4'
27
require 'ec2/amitools/fileutil'
28
require 'ec2/amitools/syschecks'
29
require 'ec2/amitools/exception'
30
require 'ec2/amitools/version'
31
require 'ec2/platform/solaris/mtab'
32
require 'ec2/platform/solaris/fstab'
33
require 'ec2/platform/solaris/constants'
39
class ExecutionError < RuntimeError
42
# This class encapsulate functionality to create an file loopback image
43
# from a volume. The image is created using mkfile. Sub-directories of the
44
# volume, including mounts of local filesystems, are copied to the image.
45
# Symbolic links are preserved wherever possible.
48
WORKSPACE = '/mnt/ec2-bundle-workspace'
49
MOUNT = File.join( WORKSPACE, 'mnt' )
50
ARCHIVE = File.join( WORKSPACE, 'archive' )
54
#---------------------------------------------------------------------#
55
def initialize( volume, # path to volume to be bundled
56
filename, # name of image file to create
57
size, # size of image file in MB
58
exclude, # list of directories to exclude
59
vfstab=nil, # file system table to use
66
if vfstab.nil? or vfstab == :legacy
67
@vfstab = EC2::Platform::Solaris::Fstab::DEFAULT
68
elsif File.exists? vfstab
69
@vfstab = IO.read(vfstab)
74
# Exclude the workspace if it is in the volume being bundled.
75
@exclude << WORKSPACE if( WORKSPACE.index(volume) == 0 )
79
#---------------------------------------------------------------------#
80
# Clone a running volume into a bootable Amazon Machine Image.
83
announce( "Cloning #{@volume} into image file #{@filename}...", true)
84
announce( 'Excluding: ', true )
85
@exclude.each { |x| announce( "\t #{x}", true ) }
97
#---------------------------------------------------------------------#
98
# Create, format and mount the blank machine image file.
99
# TODO: investigate parallelizing prepare() with archive()
101
FileUtils.mkdir_p( MOUNT )
102
announce( 'Creating and formatting file-system image...', true )
103
evaluate( "/usr/sbin/mkfile #{@size*1024*1024} #{@filename}" )
105
announce( 'Formatting file-system image...' )
106
execute( 'sync && devfsadm -C' )
107
@device = evaluate('/usr/sbin/lofiadm -a ' + @filename).strip
108
number = @device.split(/\//).last rescue nil
109
raise FatalError.new('Failed to attach image to a device' ) unless number
110
execute( "echo y | newfs /dev/rlofi/#{number} < /dev/null > /dev/null 2>&1", true )
113
mount( @device, MOUNT )
116
#---------------------------------------------------------------------#
117
# Create a flash archive of the system at the desired volume root.
119
FileUtils.mkdir_p( WORKSPACE )
120
announce( 'Creating flash archive of file system...', true )
122
@exclude.each do |item|
123
item = File.expand_path(item)
124
# Since flarcreate does not allow you to exclude a mount-point from
125
# a flash archive, we work around this by listing the files in that
126
# directory and excluding them individually.
128
exempt.concat( evaluate( 'ls -A ' + item).split(/\s/).map{|i| File.join( item, i ) } )
133
exempt = exempt.join( ' -x ')
135
invocation = ['flar create -n ec2.archive -S -R ' + @volume ]
136
invocation << ( '-x ' + exempt ) unless exempt.empty?
137
invocation << ARCHIVE
138
evaluate( invocation.join( ' ' ) )
139
raise FatalError.new( "Archive creation failed" ) unless File.exist?( ARCHIVE )
141
asize = FileUtil.size( ARCHIVE ) / ( 1024 * 1024 )
142
raise FatalError.new( "Archive too small" ) unless asize > 0
143
raise FatalError.new( 'Archive exceeds target size' ) if asize > @size
146
#---------------------------------------------------------------------#
147
# Extract the archive into the file-system image.
149
announce( 'Replicating archive to image (this will take a while)...', true )
150
# Extract flash archive into mounted image. The flar utility places
151
# the output in a folder called 'archive'. Since we cannot override
152
# this, we need to extract the content, move it to the image root
153
# and delete remove the cruft
154
extract = File.join( MOUNT, 'archive')
155
execute( "flar split -S archive -d #{MOUNT} -f #{ARCHIVE}" )
156
execute( "ls -A #{extract} | xargs -i mv #{extract}/'{}' #{MOUNT}" )
157
FileUtils.rm_rf( File.join(MOUNT, 'archive') )
158
FileUtils.rm_rf( File.join(MOUNT, 'identification') )
160
announce 'Saving system configuration...'
161
['/boot/solaris/bootenv.rc', '/etc/vfstab', '/etc/path_to_inst'].each do |path|
162
file = File.join( MOUNT, path )
163
FileUtils.cp( file, file + '.phys' )
166
announce 'Fine-tuning system configuration...'
167
execute( '/usr/sbin/sys-unconfig -R ' + MOUNT )
168
bootenv = File.join( MOUNT, '/boot/solaris/bootenv.rc' )
169
execute( "sed '/setprop bootpath/,/setprop console/d' < #{bootenv}.phys > #{bootenv}" )
170
execute( "sed '/dsk/d' < #{MOUNT}/etc/vfstab.phys > #{MOUNT}/etc/vfstab" )
172
FileUtils.rm_f( File.join(MOUNT, '/etc/rc2.d/S99dtlogin') )
174
announce 'Creating missing image directories...'
175
[ '/dev/dsk', '/dev/rdsk', '/dev/fd', '/etc/mnttab', ].each do |item|
176
FileUtils.mkdir_p( File.join( MOUNT, item ) )
179
FileUtils.ln_s( '../../devices/xpvd/xdf@0:a', File.join( MOUNT, '/dev/dsk/c0d0s0' ) )
180
FileUtils.ln_s( '../../devices/xpvd/xdf@0:a,raw', File.join( MOUNT, '/dev/rdsk/c0d0s0' ) )
182
FileUtils.touch( File.join( MOUNT, Mtab::LOCATION ) )
183
fstab = File.join( MOUNT, Fstab::LOCATION )
184
File.open(fstab, 'w+') {|io| io << @vfstab }
185
announce( "--->/etc/vfstab<---:\n" + @vfstab , true )
187
execute( "bootadm update-archive -R #{MOUNT} > /dev/null 2>&1", true )
189
announce( 'Disable xen services' )
190
file = File.join( MOUNT, '/var/svc/profile/upgrade' )
191
execute( 'echo "/usr/sbin/svcadm disable svc:/system/xctl/xend:default" >> ' + file )
193
announce 'Setting up DHCP boot'
194
FileUtils.touch( File.join( MOUNT, '/etc/hostname.xnf0' ) )
195
FileUtils.touch( File.join( MOUNT, '/etc/dhcp.xnf0' ) )
197
announce 'Setting keyboard layout'
198
kbd = File.join( MOUNT, '/etc/default/kbd' )
199
execute( "egrep '^LAYOUT' #{kbd} || echo 'LAYOUT=US-English' >> #{kbd}" )
202
#---------------------------------------------------------------------#
203
# Mount the specified device. The mount point is created if necessary.
204
# We let mount guess the appropriate file system type.
205
def mount(device, mpoint)
206
FileUtils.mkdir_p(mpoint) if not FileUtil::exists?(mpoint)
207
raise FatalError.new("image already mounted") if mounted?(mpoint)
209
execute( 'mount ' + device + ' ' + mpoint )
212
#---------------------------------------------------------------------#
214
def unmount(mpoint, force=false)
216
execute( 'sync && sync && sync' )
217
if mounted?( mpoint ) then
218
execute( 'umount ' + (force ? '-f ' : '') + mpoint )
222
#---------------------------------------------------------------------#
225
EC2::Platform::Solaris::Mtab.load.entries.keys.include? mpoint
228
#---------------------------------------------------------------------#
229
# Cleanup after self:
230
# - unmount relevant mount points.
231
# - release any device and resources attached to the image and mount-point
232
# - delete any intermediate files and directories.
237
rescue ExecutionError
238
announce "Unable to unmount image. Retrying after a short sleep."
240
if attempts < RETRIES
244
announce( "Unable to unmount image after #{RETRIES} attempts. Baling out...", true )
245
unmount( MOUNT, true )
246
if File.exist?( @filename )
247
announce( "Deleting image file #{@filename}..." )
248
FileUtils.rm_f( @filename )
253
devices = evaluate( 'lofiadm' ).split( /\n/ )
254
devices.each do |item|
255
execute( 'lofiadm -d' + @device ) if item.index( @device ) == 0
258
execute( 'devfsadm -C' )
259
FileUtils.rm_rf( WORKSPACE ) if File.directory?( WORKSPACE )
262
#---------------------------------------------------------------------#
263
# Output a message if running in debug mode
264
def announce(something, force=false)
265
STDOUT.puts( something ) if @debug or force
268
#---------------------------------------------------------------------#
269
# Execute the command line passed in.
270
def execute( cmd, verbattim = false )
273
invocation << ' 2>&1 > /dev/null' unless verbattim
274
announce( "Executing: '#{cmd}' " )
276
raise ExecutionError.new( "Failed to execute '#{cmd}'.") unless system( invocation.join )
277
announce( "Time: #{Time.now - time}s", PROFILING )
280
#---------------------------------------------------------------------#
281
# Execute command line passed in and return STDOUT output if successful.
282
def evaluate( cmd, success = 0, verbattim = false )
284
cmd << ' 2> /dev/null' unless verbattim
285
announce( "Evaluating: '#{cmd}' " )
287
pid, stdin, stdout, stderr = Open4::popen4( cmd )
289
pid, status = Process::waitpid2 pid
290
unless status.exitstatus == success
291
raise ExecutionError.new( "Failed to evaluate '#{cmd }'. Reason: #{stderr.read}." )
293
announce( "Time: #{Time.now - time}s", PROFILING )
297
def ignore(stuff) stuff end