Class: EC2::Platform::Linux::Image

Inherits:
Object
  • Object
show all
Defined in:
lib/ec2/platform/linux/image.rb

Overview

This class encapsulate functionality to create an file loopback image from a volume. The image is created using dd. Sub-directories of the volume, including mounts of local filesystems, are copied to the image. Symbolic links are preserved.

Constant Summary collapse

IMG_MNT =
'/mnt/img-mnt'
EXCLUDES =
['/dev', '/media', '/mnt', '/proc', '/sys']
DEFAULT_FSTAB =
EC2::Platform::Linux::Fstab::DEFAULT
LEGACY_FSTAB =
EC2::Platform::Linux::Fstab::LEGACY
BASE_UTILS =
[ 'modprobe', 'mount', 'umount', 'dd' ]
PART_UTILS =
[ 'dmsetup', 'kpartx', 'losetup' ]
CHROOT_UTILS =
[ 'grub' ]
ROOT_DEVICE_REGEX =

———————————————————————# Assign an appropriate partition type. The current implementation will fail to bundle volumes that reside on devices whose partition schemes deviate from what is commonly available in EC2, namely a partitioned disk with the root file system residing on the first partition.

/^(\/dev\/(?:xvd|sd)(?:[a-z]|[a-c][a-z]|d[a-x]))[1]?$/

Instance Method Summary collapse

Constructor Details

#initialize(volume, image_filename, mb_image_size, exclude, includes, filter = true, fstab = nil, part_type = nil, arch = nil, script = nil, debug = false, grub_config = nil) ⇒ Image

Initialize the instance with the required parameters.

  • volume The path to the volume to create the image file from.

  • image_filename The name of the image file to create.

  • mb_image_size The image file size in MB.

  • exclude List of directories to exclude.

  • debug Make extra noise.



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
# File 'lib/ec2/platform/linux/image.rb', line 46

def initialize( volume,
                image_filename,
                mb_image_size,
                exclude,
                includes,
                filter = true,
                fstab = nil,
                part_type = nil,
                arch = nil,
                script = nil,
                debug = false,
                grub_config = nil )
  @volume = volume
  @image_filename = image_filename
  @mb_image_size = mb_image_size
  @exclude = exclude
  @includes = includes
  @filter = filter
  @arch = arch || EC2::Platform::Linux::Uname.platform
  @script = script
  @fstab = nil
  @conf = grub_config
  @warnings = Array.new

  self.verify_runtime(BASE_UTILS)
  self.set_partition_type(part_type)

  # Cunning plan or horrible hack?
  # If :legacy is passed in as the fstab, we use the old v3 manifest's
  # device naming and fstab.
  if [:legacy, :default].include? fstab
    @fstab = fstab
  elsif not fstab.nil?
    @fstab = File.open(fstab).read()
  end
  @debug = debug

  # Exclude the temporary image mount point if it is under the volume
  # being bundled.
  if IMG_MNT.index( volume ) == 0
    @exclude << IMG_MNT
  end
end

Instance Method Details

#check_deps(part_type) ⇒ Object



201
202
203
204
205
206
207
208
209
# File 'lib/ec2/platform/linux/image.rb', line 201

def check_deps(part_type)
    if part_type == EC2::Platform::PartitionType::MBR
      self.verify_runtime([ 'parted' ])
      self.verify_runtime(CHROOT_UTILS, @volume)
    elsif part_type == EC2::Platform::PartitionType::GPT
      self.verify_runtime([ 'sgdisk' ])
      self.verify_runtime(CHROOT_UTILS, @volume)
    end
end

#check_kernel_parameters(conf) ⇒ Object



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
# File 'lib/ec2/platform/linux/image.rb', line 126

def check_kernel_parameters(conf)
  cmdline = File.read('/proc/cmdline')
  cmdline_hash = make_hash(tokenize(cmdline))

  default = nil
  File.readlines(conf).each do |line|
    match = line.match(/^default.*([\d+])/)
    if match
      default = match.captures[0]
    end
  end
  if not default
    STDERR.puts "Couldn't find default kernel designation in grub config. The resulting image may not boot."
    return
  end
  default = default.to_i

  kernels = File.readlines(conf).grep(/^\s*kernel/)
  kernel_line = kernels[default]

  kernel_line = remove_kernel(tokenize(kernel_line))
  kernel_hash = make_hash(kernel_line)

  compare_hashes(cmdline_hash, kernel_hash)

  if not @warnings.empty?
    $stdout.puts "Found the following differences between your kernel " +
                 "commandline and the grub configuration on the volume:"

    @warnings.each do |warning|
      $stdout.puts warning
    end

    $stdout.puts "Please verify that the kernel command line in " +
                 "#{File.expand_path(conf)} is correct for your new AMI."
  end
end

#compare_hashes(a, b) ⇒ Object



102
103
104
105
106
107
108
109
110
# File 'lib/ec2/platform/linux/image.rb', line 102

def compare_hashes(a, b)
  a.each do |key,value|
    if not b.has_key?(key)
      @warnings.push "\t* Missing key '#{key}' with value '#{value}'"
    elsif b[key] != value
      @warnings.push "\t* Key '#{key}' value '#{b[key]}' differs from /proc/cmdline value '#{value}'"
    end
  end
end

#is_disk_image?Boolean

———————————————————————# Returns true if we are trying to build a valid disk image

Returns:

  • (Boolean)


324
325
326
# File 'lib/ec2/platform/linux/image.rb', line 324

def is_disk_image?
  EC2::Platform::PartitionType.valid?(@part_type)
end

#makeObject

Create the loopback image file and copy volume to it.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/ec2/platform/linux/image.rb', line 165

def make
  begin
    puts( "Copying #{@volume} into the image file #{@image_filename}...")
    puts( 'Excluding: ' )
    @exclude.each { |x| puts( "\t #{x}" ) }

    create_image_file
    format_image
    execute( 'sync' )  # Flush so newly formatted filesystem is ready to mount.
    mount_image
    copy_rec( @volume, IMG_MNT)
    update_fstab
    customize_image
    finalize_image
  ensure
    cleanup
  end
end

#make_hash(array) ⇒ Object

——————————————————————–#



92
93
94
95
96
97
98
99
100
# File 'lib/ec2/platform/linux/image.rb', line 92

def make_hash(array)
  hash = Hash.new
  array.each do |entry|
    # Split on the first '='
    key, value = entry.split('=', 2)
    hash[key] = value || ''
  end
  hash
end

#remove_kernel(tokens) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/ec2/platform/linux/image.rb', line 112

def remove_kernel(tokens)
  if tokens.include?('kernel')
    tokens.delete('kernel')
    # The kernel can receive optional arguments, drop those along with the kernel
    kernel_index = tokens.index{ |token| !token.include?('--') }
    tokens = tokens.drop(kernel_index + 1)
  end
  tokens
end

#set_partition_type(input) ⇒ Object



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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ec2/platform/linux/image.rb', line 218

def set_partition_type(input)
  input ||= EC2::Platform::PartitionType::NONE
  if input == EC2::Platform::PartitionType::NONE
    # We are not doing anything interesting. Return early.
    puts('Not partitioning boot device.')
    @part_type = EC2::Platform::PartitionType::NONE
    return
  end

  # Verify that general partitioning utilities are present
  self.verify_runtime(PART_UTILS)

  value = input
  puts('Setting partition type to bundle "%s" with...' % [@volume])
  if File.directory?(@volume)
    mtab = EC2::Platform::Linux::Mtab.load
    entry = mtab.entries[@volume]
    if entry
      # Volume is a mounted file system:
      # * Determine the mounted device
      # * Ensure device partition scheme is one that we understand
      # * Ensure device and it's container(if applicable) are block devices
      # * Determine the current partition type using parted if appropriate.
      device = entry.device
      root = nil
      if (match = ROOT_DEVICE_REGEX.match(device))
        root = match[1]
        root = device unless File.exists?(root) # classic AMI with no partition table
        # Be paranoid. Bail unless the device and parent are block devices
        [device, root].each do |dev|
          unless File.blockdev?(dev)
            raise FatalError.new('Not a block device: %s' % [dev])
          end
        end
      else
        raise FatalError.new('Non-standard volume device "%s"' % [device])
      end
      if input == :auto
        self.verify_runtime([ 'parted' ])
        puts('Auto-detecting partition type for "%s"' % [@volume])
        cmd = "parted -s %s print|awk -F: '/^Partition Table/{print $2}'" % [root]
        value = evaluate(cmd).strip
        raise FatalError.new('Cannot determine partition type') if value.empty?
        puts('Partition label detected using parted: "%s"' % value)
      end
    else
      # Volume specified is possibly a file system root:
      #   * Proceed cautiously using sane defaults if no partition type
      #     has been provided.
      puts('Volume "%s" is not a mount point.'  % [@volume])
      value = EC2::Platform::PartitionType::GPT if input == :auto
      puts('Treating it as a file system root and using "%s"...' % [value])
    end
  elsif File.blockdev?(@volume)
    # Volume specified is a block device:
    #   * Not sure how we got here.
    #   * We only support bundling of file system roots and not block
    #     devices, so throw an exception.
    raise FatalError('Volume cannot be a block device "%s".' % [@volume])
  else
    # Volume specified is not a file system (mounted or otherwise):
    #   * Bail!
    raise FatalError.new('Cannot determine partition type of "%s"' % [@volume])
  end

  if EC2::Platform::PartitionType.valid?(value)
    @part_type = value
  elsif value == 'msdos'
    # This is the parted label value reported for MBR partition tables.
    @part_type = EC2::Platform::PartitionType::MBR
  elsif value == 'loop'
    # This typically indicates that we have a bare partition that is not
    # part of a partition table. This is typically the case for pv amis.
    @part_type = EC2::Platform::PartitionType::NONE
  elsif value == 'gpt'
    @part_type = EC2::Platform::PartitionType::GPT
  elsif value
    if input == :auto
      # Somehow we failed to determine a partition type that we support
      raise FatalError.new('Could not determine a suitable partition type')
    else
      # User specified a format we currently do not support. Bail.
      raise FatalError.new('Unsupported partition table type %s' % input)
    end
  else
    raise FatalError.ne('Cannot determine partition type for %s' % [@volume])
  end
  puts('Using partition type "%s"' % @part_type)

  self.check_deps(@part_type)
end

#settleObject

———————————————————————#



312
313
314
315
316
317
318
319
320
# File 'lib/ec2/platform/linux/image.rb', line 312

def settle
  # Run sync and udevadm settle to quiet device.
  execute('sync||:')
  if File.executable?('/usr/sbin/udevsettle')
    execute('/usr/sbin/udevsettle||:')
  elsif File.executable?('/sbin/udevadm')
    execute('/sbin/udevadm settle||:')
  end
end

#tokenize(string) ⇒ Object



122
123
124
# File 'lib/ec2/platform/linux/image.rb', line 122

def tokenize(string)
  string.strip.split(/\s+/)
end

#verify_runtime(utils, chroot = nil) ⇒ Object

———————————————————————# Ensure we have the specified commonly-needed utils in the PATH.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/ec2/platform/linux/image.rb', line 186

def verify_runtime(utils, chroot = nil)
  unless ENV['PATH']
    raise FatalError.new('PATH not set, cannot find needed utilities')
  end

  paths = ENV['PATH'].split(File::PATH_SEPARATOR)
  paths.map! { |path| File.join(chroot, path) } if chroot

  utils.each do |util|
    unless paths.any? { |dir| File.executable?(File.join(dir, util)) }
      raise FatalError.new("Required utility '%s' not found in PATH - is it installed?" % util)
    end
  end
end