module Bundle

Module containing utility methods for bundling an AMI.

Module containing utility methods for bundling an AMI.

Constants

CHUNK_SIZE

Public Class Methods

bundle_image( image_file, user, arch, image_type, destination, user_private_key_path, user_cert_path, ec2_cert_path, prefix, optional_args, debug = false, inherit = true ) click to toggle source
# File lib/ec2/amitools/bundle.rb, line 33
def self.bundle_image( image_file,
                       user,
                       arch,
                       image_type,
                       destination,
                       user_private_key_path,
                       user_cert_path,
                       ec2_cert_path,
                       prefix,
                       optional_args,
                       debug = false,
                       inherit = true
                     )
  begin
    raise "invalid image-type #{image_type}" unless image_type.is_a? Bundle::ImageType
    # Create named pipes.
    digest_pipe = File::join('/tmp', "ec2-bundle-image-digest-pipe-#{$$}")
    File::delete(digest_pipe) if File::exist?(digest_pipe)
    unless system( "mkfifo #{digest_pipe}" )
      raise "Error creating named pipe #{digest_pipe}"
    end
    
    # If the prefix differs from the file name create a symlink
    # so that the file is tarred with the prefix name.
    if prefix and File::basename( image_file ) != prefix
      image_file_link = File::join( destination, prefix )
      begin
        FileUtils.ln_s(image_file, image_file_link)
      rescue Exception => e
        raise "Error creating symlink to image file, #{e.message}."
      end
      image_file = image_file_link
    end
    
    # Load and generate necessary keys.
    name = prefix || File::basename( image_file )
    manifest_file = File.join( destination, name + '.manifest.xml')
    bundled_file_path = File::join( destination, name + '.tar.gz.enc' )
    user_public_key = Crypto::certfile2pubkey( user_cert_path )
    ec2_public_key = Crypto::certfile2pubkey( ec2_cert_path )
    key = Format::bin2hex( Crypto::gensymkey )
    iv = Format::bin2hex( Crypto::gensymkey )
    
    # Bundle the AMI.
    # The image file is tarred - to maintain sparseness, gzipped for
    # compression and then encrypted with AES in CBC mode for
    # confidentiality.
    # To minimize disk I/O the file is read from disk once and
    # piped via several processes. The tee is used to allow a
    # digest of the file to be calculated without having to re-read
    # it from disk.
    tar = EC2::Platform::Current::Tar::Command.new.create.dereference.sparse
    tar.owner(0).group(0)
    tar.add(File::basename( image_file ), File::dirname( image_file ))
    openssl = EC2::Platform::Current::Constants::Utility::OPENSSL
    pipeline = EC2::Platform::Current::Pipeline.new('image-bundle-pipeline', debug)
    pipeline.concat([
      ['tar', "#{openssl} sha1 < #{digest_pipe} & " + tar.expand],
      ['tee', "tee #{digest_pipe}"],
      ['gzip', 'gzip -9'],
      ['encrypt', "#{openssl} enc -e -aes-128-cbc -K #{key} -iv #{iv} > #{bundled_file_path}"]
      ])
    digest = nil
    begin
      digest = pipeline.execute.split(/\s+/).last.strip
    rescue EC2::Platform::Current::Pipeline::ExecutionError => e
      $stderr.puts e.message
      exit 1
    end

    # Split the bundled AMI.
    # Splitting is not done as part of the compress, encrypt digest
    # stream, so that the filenames of the parts can be easily
    # tracked. The alternative is to create a dedicated output
    # directory, but this leaves the user less choice.
    parts = Bundle::split( bundled_file_path, name, destination )
    
    # Sum the parts file sizes to get the encrypted file size.
    bundled_size = 0
    parts.each do |part|
      bundled_size += File.size( File.join( destination, part ) )
    end
    
    # Encrypt key and iv.
    padding = OpenSSL::PKey::RSA::PKCS1_PADDING
    user_encrypted_key = user_public_key.public_encrypt( key, padding )
    ec2_encrypted_key = ec2_public_key.public_encrypt( key, padding )
    user_encrypted_iv = user_public_key.public_encrypt( iv, padding )
    ec2_encrypted_iv = ec2_public_key.public_encrypt( iv, padding )

    # Digest parts.
    part_digest_list = Bundle::digest_parts( parts, destination )
    
    # Launch-customization data
    patch_in_instance_meta_data(image_type, optional_args) if inherit

    # Sanity-check block-device-mappings
    bdm = optional_args[:block_device_mapping]
    if bdm.is_a? Hash
      [ 'root', 'ami' ].each do |item|
        if bdm[item].to_s.strip.empty?
          $stdout.puts "Block-device-mapping has no '#{item}' entry. A launch-time default will be used."
        end
      end
    end
 
 
    # Create bundle manifest.
    $stdout.puts 'Creating bundle manifest...'
    manifest = ManifestV20071010.new()
    manifest.init(optional_args.merge({:name => name,
                   :user => user,
                   :image_type => image_type.to_s,
                   :arch => arch,
                   :reserved => nil,
                   :parts => part_digest_list,
                   :size => File::size( image_file ),
                   :bundled_size => bundled_size,
                   :user_encrypted_key => Format::bin2hex( user_encrypted_key ),
                   :ec2_encrypted_key => Format::bin2hex( ec2_encrypted_key ),
                   :cipher_algorithm => Crypto::SYM_ALG,
                   :user_encrypted_iv => Format::bin2hex( user_encrypted_iv ),
                   :ec2_encrypted_iv => Format::bin2hex( ec2_encrypted_iv ),
                   :digest => digest,
                   :digest_algorithm => Crypto::DIGEST_ALG,
                   :privkey_filename => user_private_key_path,
                   :kernel_id => optional_args[:kernel_id],
                   :ramdisk_id => optional_args[:ramdisk_id],
                   :product_codes => optional_args[:product_codes],
                   :ancestor_ami_ids => optional_args[:ancestor_ami_ids],
                   :block_device_mapping => optional_args[:block_device_mapping],
                   :bundler_name => EC2Version::PKG_NAME,
                   :bundler_version => EC2Version::PKG_VERSION,
                   :bundler_release  => EC2Version::PKG_RELEASE}))
    
    # Write out the manifest file.
    File.open( manifest_file, 'w' ) { |f| f.write( manifest.to_s ) }
    $stdout.puts 'Bundle manifest is %s' % manifest_file
  ensure
    # Clean up.
    if bundled_file_path and File.exist?( bundled_file_path )
      File.delete( bundled_file_path )
    end
    File::delete( digest_pipe ) if digest_pipe and File::exist?(digest_pipe)
    if image_file_link and File::exist?( image_file_link )
      File::delete( image_file_link )
    end
  end
end
digest_parts( basenames, dir ) click to toggle source
# File lib/ec2/amitools/bundle.rb, line 240
def self.digest_parts( basenames, dir )
  $stdout.puts 'Generating digests for each part...'
  parts_digests = Array.new
  basenames.each do |basename|
    File.open(File.join(dir, basename)) do |f|
      parts_digests << [basename, Crypto.digest( f )]
    end
  end
  $stdout.puts 'Digests generated.'
  parts_digests
end
patch_in_instance_meta_data(image_type, optional_args) click to toggle source
# File lib/ec2/amitools/bundle.rb, line 183
def self.patch_in_instance_meta_data(image_type, optional_args)
  if (image_type == ImageType::VOLUME || image_type == ImageType::MACHINE )
    instance_data = EC2::InstanceData.new
    if !instance_data.instance_data_accessible
      raise "Error accessing instance data. If you are not bundling on an EC2 instance use --no-inherit." 
    else
      [
        [:ancestor_ami_ids,     instance_data.ancestor_ami_ids, Proc.new do |key, value|
          if (optional_args[key].nil?)
            ancestry = nil
            if value.nil? or value.to_s.empty?
              ancestry = []
            elsif value.is_a? Array
              ancestry = value
            else
              ancestry = [value]
            end
            ami_id = instance_data.ami_id
            $stdout.puts "Unable to read instance meta-data for ami-id" if ami_id.nil?
            ancestry << ami_id unless(ami_id.nil? or ancestry.include?(ami_id))
            optional_args[key] = ancestry if ancestry && ancestry.length > 0
          end
        end],
        [:kernel_id,            instance_data.kernel_id, nil],
        [:ramdisk_id,           instance_data.ramdisk_id, nil],
        [:product_codes,        instance_data.product_codes, nil],
        [:block_device_mapping, instance_data.block_device_mapping, nil],
      ].each do |key, value, block|
        begin
          if value.nil?
            $stdout.puts "Unable to read instance meta-data for #{key.to_s.gsub('_','-')}"
            block.call(key, value) if block
          else
            if block
              block.call(key, value)
            else
              optional_args[key] ||= value
            end
          end
        rescue
          $stdout.puts "Unable to set #{key.to_s.gsub('_','-')} from instance meta-data"
        end
      end
    end
  end
end
split( filename, prefix, destination ) click to toggle source
# File lib/ec2/amitools/bundle.rb, line 230
def self.split( filename, prefix, destination )
  $stdout.puts "Splitting #{filename}..."
  part_filenames = FileUtil::split(filename,
                                   prefix,
                                   CHUNK_SIZE,
                                   destination)
  part_filenames.each { |name| puts "Created #{name}" }
  part_filenames
end