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

« back to all changes in this revision

Viewing changes to lib/ec2/amitools/uploadbundle.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
require 'ec2/amitools/crypto'
 
12
require 'ec2/amitools/exception'
 
13
require 'ec2/common/http'
 
14
require 'ec2/amitools/uploadbundleparameters'
 
15
require 'rexml/document'
 
16
require 'tempfile'
 
17
require 'uri'
 
18
 
 
19
NAME = 'ec2-upload-bundle'
 
20
 
 
21
#------------------------------------------------------------------------------#
 
22
 
 
23
MANUAL =<<TEXT
 
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)
 
29
 
 
30
#{NAME} will:
 
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)
 
36
 
 
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
 
40
TEXT
 
41
 
 
42
BACKOFF_PERIOD = 5
 
43
 
 
44
#------------------------------------------------------------------------------#
 
45
 
 
46
class MakeBucketError < RuntimeError
 
47
  def initialize( bucket, rsp )
 
48
    super "Could not create bucket #{bucket}, server response:\n #{rsp}"
 
49
  end
 
50
end
 
51
 
 
52
#------------------------------------------------------------------------------#
 
53
 
 
54
class UploadFileError < RuntimeError
 
55
  def initialize( file )
 
56
    super "Could not upload file: #{file}"
 
57
  end
 
58
end
 
59
 
 
60
#----------------------------------------------------------------------------#
 
61
 
 
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}"
 
66
 
 
67
  loop do
 
68
    begin
 
69
      EC2::Common::HTTP::put( url, path, {"x-amz-acl"=>acl}, user, pass, debug )
 
70
      break
 
71
    rescue EC2::Common::HTTP::Error::PathInvalid => e
 
72
      raise "Error: no such file \"#{path}\""
 
73
    rescue RuntimeError => e
 
74
      if retry_upload
 
75
        STDERR.puts "Failed to upload #{file}, #{e.message}"
 
76
        STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
 
77
        sleep BACKOFF_PERIOD
 
78
      else
 
79
        raise "Error: failed to upload \"#{path}\", #{e.message}"
 
80
      end
 
81
    end
 
82
  end
 
83
  
 
84
  return url
 
85
end
 
86
 
 
87
#----------------------------------------------------------------------------#
 
88
 
 
89
# Return a list of bundle part filename and part number tuples from the manifest.
 
90
def get_part_info( manifest )
 
91
  parts = Array.new
 
92
 
 
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 ]
 
96
  end
 
97
  parts.sort
 
98
  parts
 
99
end
 
100
 
 
101
#------------------------------------------------------------------------------#
 
102
 
 
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 )
 
107
end
 
108
 
 
109
#------------------------------------------------------------------------------#
 
110
 
 
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 )
 
113
 
 
114
  path = File.join( s3url, bucket ) + "/?acl"
 
115
  
 
116
  begin
 
117
    response = nil
 
118
    options = {'Content-Length' => '0'}
 
119
    loop do
 
120
      begin
 
121
        
 
122
        response = EC2::Common::HTTP::get(path, nil, options, user, pass, nil, nil, debug)
 
123
        if response.success?
 
124
          begin
 
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.
 
133
              end
 
134
            end
 
135
          rescue RuntimeError => e
 
136
            raise "Could not parse ACL response from server, #{e.message}"
 
137
          end
 
138
        else
 
139
          return false  
 
140
        end        
 
141
        break
 
142
      rescue EC2::Common::HTTP::Error::Retrieve => e
 
143
        return false if e.code == 404
 
144
        raise e
 
145
      rescue StandardError => e # Communication error.
 
146
        if retry_create
 
147
          STDERR.puts "Failed to access bucket \"#{bucket}\" #{e.message}"
 
148
          STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
 
149
          sleep BACKOFF_PERIOD
 
150
        else
 
151
          raise e
 
152
        end
 
153
      end
 
154
    end
 
155
  end
 
156
end
 
157
 
 
158
#------------------------------------------------------------------------------#
 
159
 
 
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 )  
 
162
  begin
 
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 )
 
168
      
 
169
      begin
 
170
        buffer = Tempfile.new('ec2-create-bucket')      
 
171
        loop do
 
172
          error = ''
 
173
          begin
 
174
            rsp = EC2::Common::HTTP::put( path, buffer.path, options, user, pass, debug )
 
175
            return true if rsp.success?
 
176
            unless retry_create
 
177
              STDERR.puts "Could not create or access bucket #{bucket}"
 
178
              STDERR.puts "Server response was #{rsp.code} (#{rsp.body})"
 
179
              return false
 
180
            end
 
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}"
 
186
          end
 
187
          
 
188
          STDERR.puts "Failed to create bucket #{bucket}#{error}"
 
189
          STDOUT.puts "Retrying in #{BACKOFF_PERIOD}s ..."
 
190
          sleep BACKOFF_PERIOD
 
191
        end        
 
192
      ensure
 
193
        buffer.unlink
 
194
      end
 
195
    end
 
196
  end
 
197
end
 
198
 
 
199
#------------------------------------------------------------------------------#
 
200
  
 
201
#
 
202
# Get parameters and display help or manual if necessary.
 
203
#
 
204
def main
 
205
  begin
 
206
    p = UploadBundleParameters.new( ARGV, NAME )    
 
207
  rescue StandardError => e
 
208
    STDERR.puts e.message
 
209
    STDERR.puts "Try '#{NAME} --help'"
 
210
    return 1
 
211
  end
 
212
  
 
213
  if p.show_help
 
214
    STDOUT.puts p.help
 
215
    return 0
 
216
  end
 
217
  
 
218
  if p.manual
 
219
    STDOUT.puts MANUAL
 
220
    return 0
 
221
  end
 
222
  
 
223
  status = 1
 
224
  begin
 
225
    # Get the S3 URL.
 
226
    s3_uri = URI.parse( p.url )
 
227
    s3_url = uri2string( s3_uri )
 
228
    retry_upload = p.retry
 
229
    
 
230
    # Create storage bucket if required.
 
231
    create_bucket( s3_url, p.bucket, p.acl, retry_upload, p.user, p.pass, p.debug )
 
232
    
 
233
    # Load manifest.
 
234
    xml = String.new
 
235
    manifest_path = p.manifest
 
236
    File.open( manifest_path ) { |f| xml << f.read }
 
237
    
 
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}"
 
246
      else
 
247
        STDOUT.puts "Skipping #{part_info[0]}"
 
248
      end
 
249
    end
 
250
    
 
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}"
 
256
    else
 
257
      STDOUT.puts "Skipping manifest."
 
258
    end
 
259
    
 
260
    status = 0
 
261
  rescue EC2::Common::HTTP::Error => e
 
262
    STDERR.puts e.message
 
263
    status = e.code
 
264
  rescue StandardError => e
 
265
    STDERR.puts e.message
 
266
    STDERR.puts e.backtrace if p.debug
 
267
  end  
 
268
  
 
269
  if status == 0
 
270
    STDOUT.puts 'Bundle upload completed.'
 
271
  else
 
272
    STDOUT.puts 'Bundle upload failed.'
 
273
  end
 
274
  
 
275
  return status
 
276
end
 
277
 
 
278
#------------------------------------------------------------------------------#
 
279
# Script entry point. Execute only if this file is being executed.
 
280
if __FILE__ == $0
 
281
  begin
 
282
    status = main
 
283
  rescue Interrupt
 
284
    STDERR.puts "\n#{NAME} interrupted."
 
285
    status = 255
 
286
  end
 
287
  exit status
 
288
end