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
require 'ec2/amitools/crypto'
12
require 'ec2/amitools/exception'
13
require 'ec2/common/http'
14
require 'ec2/amitools/uploadbundleparameters'
15
require 'rexml/document'
19
NAME = 'ec2-upload-bundle'
21
#------------------------------------------------------------------------------#
24
#{NAME} is a command line tool to upload a bundled Amazon Image to S3 storage
25
for use by EC2. An Amazon Image may be one of the following:
26
- Amazon Machine Image (AMI)
27
- Amazon Kernel Image (AKI)
28
- Amazon Ramdisk Image (ARI)
31
- encrypt the AMI manifest with EC2's public key
32
- sign the AMI manifest with the user's private key
33
- create an S3 bucket to store the bundled AMI in if it does not already exist
34
- upload the AMI manifest and parts files to S3, granting specified privileges
35
- on them (defaults to EC2 read privileges)
37
To manually retry an upload that failed, #{NAME} can optionally:
38
- skip uploading the manifest
39
- only upload bundled AMI parts from a specified part onwards
44
#------------------------------------------------------------------------------#
46
class MakeBucketError < RuntimeError
47
def initialize( bucket, rsp )
48
super "Could not create bucket #{bucket}, server response:\n #{rsp}"
52
#------------------------------------------------------------------------------#
54
class UploadFileError < RuntimeError
55
def initialize( file )
56
super "Could not upload file: #{file}"
60
#----------------------------------------------------------------------------#
62
# Upload the specified file.
63
def upload( s3_url, bucket, file, path, acl, retry_upload, user = nil, pass = nil, debug=false )
64
basename = File::basename( file )
65
url = "#{s3_url}/#{bucket}/#{basename}"
69
EC2::Common::HTTP::put( url, path, {"x-amz-acl"=>acl}, user, pass, debug )
71
rescue EC2::Common::HTTP::Error::PathInvalid => e
72
raise "Error: no such file \"#{path}\""
73
rescue RuntimeError => e
75
STDERR.puts "Failed to upload #{file}, #{e.message}"
76
STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
79
raise "Error: failed to upload \"#{path}\", #{e.message}"
87
#----------------------------------------------------------------------------#
89
# Return a list of bundle part filename and part number tuples from the manifest.
90
def get_part_info( manifest )
93
REXML::XPath.each( manifest.root, 'image/parts/part' ) do|part|
94
e = REXML::XPath.first(part, 'filename')
95
parts << [e.text, part.attribute( 'index' ).to_s.to_i ]
101
#------------------------------------------------------------------------------#
103
def uri2string( uri )
104
s = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}"
105
# Remove the trailing '/'.
106
return ( s[s.size - 1 ] == 47 ? s.slice( 0..( s.size - 2 ) ) : s )
109
#------------------------------------------------------------------------------#
111
# Check if the bucket exists and the necessary ACL is set.
112
def bucket_set?( s3url, bucket, retry_create, user = nil, pass = nil, debug = false )
114
path = File.join( s3url, bucket ) + "/?acl"
118
options = {'Content-Length' => '0'}
122
response = EC2::Common::HTTP::get(path, nil, options, user, pass, nil, nil, debug)
125
STDOUT.puts response.body if debug and response.text?
126
doc = REXML::Document.new( response.body )
127
REXML::XPath.each( doc.root, 'AccessControlList/Grant' ) do |grant|
128
if ( name = REXML::XPath.first( grant, 'Grantee/DisplayName') and
129
name.text == 'za-team' and
130
permission = REXML::XPath.first( grant, 'Permission' ) and
131
permission.text == 'READ' )
132
return true # ACL is set.
135
rescue RuntimeError => e
136
raise "Could not parse ACL response from server, #{e.message}"
142
rescue EC2::Common::HTTP::Error::Retrieve => e
143
return false if e.code == 404
145
rescue StandardError => e # Communication error.
147
STDERR.puts "Failed to access bucket \"#{bucket}\" #{e.message}"
148
STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
158
#------------------------------------------------------------------------------#
160
# Create the specified bucket if it does not exist.
161
def create_bucket( s3url, bucket, acl, retry_create, user = nil, pass = nil, debug=false )
163
unless bucket_set?( s3url, bucket, retry_create, user, pass, debug )
164
STDOUT.puts "Setting bucket ACL to allow EC2 read access ..."
165
options = {'Content-Length' => '0'}
166
options['x-amz-acl'] = acl if user and pass
167
path = File.join( s3url, bucket )
170
buffer = Tempfile.new('ec2-create-bucket')
174
rsp = EC2::Common::HTTP::put( path, buffer.path, options, user, pass, debug )
175
return true if rsp.success?
177
STDERR.puts "Could not create or access bucket #{bucket}"
178
STDERR.puts "Server response was #{rsp.code} (#{rsp.body})"
181
raise "HTTP PUT returned #{rsp.code}."
182
rescue EC2::Common::HTTP::Error::Retrieve => e
183
error = ": server response #{e.message} #{e.code}"
184
rescue RuntimeError => e
185
error = ": error message #{e.message}"
188
STDERR.puts "Failed to create bucket #{bucket}#{error}"
189
STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
199
#------------------------------------------------------------------------------#
202
# Get parameters and display help or manual if necessary.
206
p = UploadBundleParameters.new( ARGV, NAME )
207
rescue StandardError => e
208
STDERR.puts e.message
209
STDERR.puts "Try '#{NAME} --help'"
226
s3_uri = URI.parse( p.url )
227
s3_url = uri2string( s3_uri )
228
retry_upload = p.retry
230
# Create storage bucket if required.
231
create_bucket( s3_url, p.bucket, p.acl, retry_upload, p.user, p.pass, p.debug )
235
manifest_path = p.manifest
236
File.open( manifest_path ) { |f| xml << f.read }
238
# Upload AMI bundle parts.
239
STDOUT.puts "Uploading bundled image parts to #{s3_url}/#{p.bucket} ..."
240
manifest = REXML::Document.new(xml)
241
get_part_info( manifest ).each do |part_info|
242
if !p.part or ( p.part and part_info[1] >= p.part )
243
path = File.join( p.directory, part_info[0] )
244
url = upload( s3_url, p.bucket, part_info[0], path, p.acl, retry_upload, p.user, p.pass, p.debug )
245
STDOUT.puts "Uploaded #{part_info[0]} to #{url}"
247
STDOUT.puts "Skipping #{part_info[0]}"
251
# Encrypt and upload manifest.
252
unless p.skipmanifest
253
STDOUT.puts "Uploading manifest ..."
254
url = upload(s3_url, p.bucket, p.manifest, manifest_path, p.acl, retry_upload, p.user, p.pass, p.debug )
255
STDOUT.puts "Uploaded manifest to #{url}"
257
STDOUT.puts "Skipping manifest."
261
rescue EC2::Common::HTTP::Error => e
262
STDERR.puts e.message
264
rescue StandardError => e
265
STDERR.puts e.message
266
STDERR.puts e.backtrace if p.debug
270
STDOUT.puts 'Bundle upload completed.'
272
STDOUT.puts 'Bundle upload failed.'
278
#------------------------------------------------------------------------------#
279
# Script entry point. Execute only if this file is being executed.
284
STDERR.puts "\n#{NAME} interrupted."