~awstools-dev/ubuntu/maverick/ec2-ami-tools/maverick

« back to all changes in this revision

Viewing changes to lib/ec2/platform/solaris/image.rb

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2008-10-14 08:35:25 UTC
  • Revision ID: james.westby@ubuntu.com-20081014083525-c0n69wr7r7aqfb8w
Tags: 1.3-26357-0ubuntu2
* New upstream version.
* Update the debian copyright file.
* Added quilt patch system to make it easier to maintain. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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.
 
10
 
 
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
 
17
 
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
#---------------------------------------------------------------------#
 
24
 
 
25
require 'fileutils'
 
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'
 
34
 
 
35
module EC2
 
36
  module Platform
 
37
    module Solaris
 
38
      
 
39
    class ExecutionError < RuntimeError
 
40
    end
 
41
      
 
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.
 
46
      class Image
 
47
        EXCLUDES  = [ '/mnt' ]
 
48
        WORKSPACE = '/mnt/ec2-bundle-workspace'
 
49
        MOUNT     = File.join( WORKSPACE, 'mnt' )
 
50
        ARCHIVE   = File.join( WORKSPACE, 'archive' )
 
51
        PROFILING = true
 
52
        RETRIES   = 5
 
53
        DELAY     = 10
 
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
 
60
                        debug = false )
 
61
          @volume = volume
 
62
          @filename = filename
 
63
          @size = size
 
64
          @exclude = exclude
 
65
          @debug = debug
 
66
          if vfstab.nil? or vfstab == :legacy
 
67
            @vfstab = EC2::Platform::Solaris::Fstab::DEFAULT
 
68
          elsif File.exists? vfstab
 
69
            @vfstab = IO.read(vfstab)
 
70
          else
 
71
            @vfstab = vfstab
 
72
          end
 
73
          
 
74
          # Exclude the workspace if it is in the volume being bundled.
 
75
          @exclude << WORKSPACE if( WORKSPACE.index(volume) == 0 )
 
76
          
 
77
        end
 
78
      
 
79
        #---------------------------------------------------------------------#        
 
80
        # Clone a running volume into a bootable Amazon Machine Image.
 
81
        def make
 
82
          begin
 
83
            announce( "Cloning #{@volume} into image file #{@filename}...", true)
 
84
            announce( 'Excluding: ', true )
 
85
            @exclude.each { |x| announce( "\t #{x}", true ) }
 
86
            archive
 
87
            prepare
 
88
            replicate
 
89
          ensure
 
90
            cleanup
 
91
          end
 
92
        end
 
93
        
 
94
        private
 
95
        
 
96
        
 
97
        #---------------------------------------------------------------------#
 
98
        # Create, format and mount the blank machine image file.
 
99
        # TODO: investigate parallelizing prepare() with archive()
 
100
        def prepare
 
101
          FileUtils.mkdir_p( MOUNT )
 
102
          announce( 'Creating and formatting file-system image...', true )
 
103
          evaluate( "/usr/sbin/mkfile #{@size*1024*1024} #{@filename}" )
 
104
          
 
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 )
 
111
          
 
112
          execute( 'sync' )
 
113
          mount( @device, MOUNT )
 
114
        end
 
115
        
 
116
        #---------------------------------------------------------------------#
 
117
        # Create a flash archive of the system at the desired volume root.
 
118
        def archive
 
119
          FileUtils.mkdir_p( WORKSPACE )
 
120
          announce( 'Creating flash archive of file system...', true )
 
121
          exempt = []
 
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.
 
127
            if mounted? item
 
128
              exempt.concat( evaluate( 'ls -A ' + item).split(/\s/).map{|i| File.join( item, i ) } )
 
129
            else
 
130
              exempt << item
 
131
            end
 
132
          end
 
133
          exempt = exempt.join( ' -x ')
 
134
 
 
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 )
 
140
          
 
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
 
144
        end
 
145
        
 
146
        #---------------------------------------------------------------------#
 
147
        # Extract the archive into the file-system image.
 
148
        def replicate
 
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') )
 
159
          
 
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' )
 
164
          end
 
165
 
 
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" )
 
171
          
 
172
          FileUtils.rm_f( File.join(MOUNT, '/etc/rc2.d/S99dtlogin') )
 
173
          
 
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 ) )
 
177
          end
 
178
          
 
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' ) )
 
181
          
 
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 )
 
186
          
 
187
          execute( "bootadm update-archive -R #{MOUNT} > /dev/null 2>&1", true )
 
188
          
 
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 )
 
192
          
 
193
          announce 'Setting up DHCP boot'
 
194
          FileUtils.touch( File.join( MOUNT, '/etc/hostname.xnf0' ) )
 
195
          FileUtils.touch( File.join( MOUNT, '/etc/dhcp.xnf0' ) )
 
196
          
 
197
          announce 'Setting keyboard layout'
 
198
          kbd = File.join( MOUNT, '/etc/default/kbd' )
 
199
          execute( "egrep '^LAYOUT' #{kbd} || echo 'LAYOUT=US-English' >> #{kbd}" )
 
200
        end
 
201
        
 
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)
 
208
          execute( 'sync' )
 
209
          execute( 'mount ' + device + ' ' + mpoint )
 
210
        end
 
211
        
 
212
        #---------------------------------------------------------------------#
 
213
        
 
214
        def unmount(mpoint, force=false)
 
215
          GC.start
 
216
          execute( 'sync && sync && sync' )
 
217
          if mounted?( mpoint ) then
 
218
            execute( 'umount ' + (force ? '-f ' : '') + mpoint )
 
219
          end
 
220
        end
 
221
      
 
222
        #---------------------------------------------------------------------#
 
223
      
 
224
        def mounted?(mpoint)
 
225
          EC2::Platform::Solaris::Mtab.load.entries.keys.include? mpoint
 
226
        end
 
227
      
 
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.
 
233
        def cleanup
 
234
          attempts = 0
 
235
          begin
 
236
            unmount( MOUNT )
 
237
          rescue ExecutionError
 
238
            announce "Unable to unmount image. Retrying after a short sleep."
 
239
            attempts += 1
 
240
            if attempts < RETRIES
 
241
              sleep DELAY
 
242
              retry
 
243
            else
 
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 )
 
249
              end
 
250
            end
 
251
          end
 
252
          unless @device.nil?
 
253
            devices = evaluate( 'lofiadm' ).split( /\n/ )
 
254
            devices.each do |item|
 
255
              execute( 'lofiadm -d' + @device ) if item.index( @device ) == 0
 
256
            end
 
257
          end
 
258
          execute( 'devfsadm -C' )
 
259
          FileUtils.rm_rf( WORKSPACE ) if File.directory?( WORKSPACE )
 
260
        end
 
261
        
 
262
        #---------------------------------------------------------------------#        
 
263
        # Output a message if running in debug mode
 
264
        def announce(something, force=false)
 
265
          STDOUT.puts( something ) if @debug or force
 
266
        end
 
267
      
 
268
        #---------------------------------------------------------------------#        
 
269
        # Execute the command line passed in.
 
270
        def execute( cmd, verbattim = false )
 
271
          verbattim ||= @debug
 
272
          invocation = [ cmd ]
 
273
          invocation << ' 2>&1 > /dev/null' unless verbattim
 
274
          announce( "Executing: '#{cmd}' " )
 
275
          time = Time.now
 
276
          raise ExecutionError.new( "Failed to execute '#{cmd}'.") unless system( invocation.join )
 
277
          announce( "Time: #{Time.now - time}s", PROFILING )
 
278
        end
 
279
      
 
280
        #---------------------------------------------------------------------#        
 
281
        # Execute command line passed in and return STDOUT output if successful.
 
282
        def evaluate( cmd, success = 0, verbattim = false )
 
283
          verbattim ||= @debug
 
284
          cmd << ' 2> /dev/null' unless verbattim
 
285
          announce( "Evaluating: '#{cmd}' " )
 
286
          time = Time.now
 
287
          pid, stdin, stdout, stderr = Open4::popen4( cmd )
 
288
          ignore stdin 
 
289
          pid, status = Process::waitpid2 pid
 
290
          unless status.exitstatus == success
 
291
            raise ExecutionError.new( "Failed to evaluate '#{cmd }'. Reason: #{stderr.read}." )
 
292
          end
 
293
          announce( "Time: #{Time.now - time}s", PROFILING )
 
294
          stdout.read
 
295
        end
 
296
        
 
297
        def ignore(stuff) stuff end
 
298
        
 
299
      end
 
300
    end
 
301
  end
 
302
end