staugaard-cloudmaster 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/VERSION.yml +4 -0
  2. data/bin/cloudmaster +45 -0
  3. data/lib/AWS/AWS.rb +3 -0
  4. data/lib/AWS/EC2.rb +14 -0
  5. data/lib/AWS/S3.rb +14 -0
  6. data/lib/AWS/SQS.rb +14 -0
  7. data/lib/AWS/SimpleDB.rb +14 -0
  8. data/lib/MockAWS/EC2.rb +119 -0
  9. data/lib/MockAWS/S3.rb +39 -0
  10. data/lib/MockAWS/SQS.rb +82 -0
  11. data/lib/MockAWS/SimpleDB.rb +46 -0
  12. data/lib/MockAWS/clock.rb +67 -0
  13. data/lib/OriginalAWS/AWS.rb +475 -0
  14. data/lib/OriginalAWS/EC2.rb +783 -0
  15. data/lib/OriginalAWS/S3.rb +559 -0
  16. data/lib/OriginalAWS/SQS.rb +159 -0
  17. data/lib/OriginalAWS/SimpleDB.rb +460 -0
  18. data/lib/RetryAWS/EC2.rb +88 -0
  19. data/lib/RetryAWS/S3.rb +77 -0
  20. data/lib/RetryAWS/SQS.rb +109 -0
  21. data/lib/RetryAWS/SimpleDB.rb +118 -0
  22. data/lib/SafeAWS/EC2.rb +63 -0
  23. data/lib/SafeAWS/S3.rb +56 -0
  24. data/lib/SafeAWS/SQS.rb +75 -0
  25. data/lib/SafeAWS/SimpleDB.rb +88 -0
  26. data/lib/aws_context.rb +165 -0
  27. data/lib/basic_configuration.rb +120 -0
  28. data/lib/clock.rb +10 -0
  29. data/lib/factory.rb +14 -0
  30. data/lib/file_logger.rb +36 -0
  31. data/lib/inifile.rb +148 -0
  32. data/lib/instance_logger.rb +25 -0
  33. data/lib/logger_factory.rb +38 -0
  34. data/lib/periodic.rb +29 -0
  35. data/lib/string_logger.rb +29 -0
  36. data/lib/sys_logger.rb +40 -0
  37. data/lib/user_data.rb +30 -0
  38. data/test/aws-config.ini +9 -0
  39. data/test/cloudmaster-tests.rb +329 -0
  40. data/test/configuration-test.rb +62 -0
  41. data/test/daytime-policy-tests.rb +47 -0
  42. data/test/enumerator-test.rb +47 -0
  43. data/test/fixed-policy-tests.rb +50 -0
  44. data/test/instance-pool-test.rb +359 -0
  45. data/test/instance-test.rb +98 -0
  46. data/test/job-policy-test.rb +95 -0
  47. data/test/manual-policy-tests.rb +63 -0
  48. data/test/named-queue-test.rb +90 -0
  49. data/test/resource-policy-tests.rb +126 -0
  50. data/test/suite +17 -0
  51. data/test/test-config.ini +47 -0
  52. metadata +111 -0
@@ -0,0 +1,559 @@
1
+ # Sample Ruby code for the O'Reilly book "Programming Amazon Web
2
+ # Services" by James Murty.
3
+ #
4
+ # This code was written for Ruby version 1.8.6 or greater.
5
+ #
6
+ # The S3 module implements the REST API of the Amazon Simple Storage Service.
7
+ require 'AWS'
8
+ require 'digest/md5'
9
+
10
+ class S3
11
+ include AWS # Include the AWS module as a mixin
12
+
13
+ S3_ENDPOINT = "s3.amazonaws.com"
14
+ XMLNS = 'http://s3.amazonaws.com/doc/2006-03-01/'
15
+
16
+ def valid_dns_name(bucket_name)
17
+ if bucket_name.size > 63 or bucket_name.size < 3
18
+ return false
19
+ end
20
+
21
+ return false unless bucket_name =~ /^[a-z0-9][a-z0-9.-]+$/
22
+
23
+ return false unless bucket_name =~ /[a-z]/ # Cannot be an IP address
24
+
25
+ bucket_name.split('.').each do |fragment|
26
+ return false if fragment =~ /^-/ or fragment =~ /-$/ or fragment =~ /^$/
27
+ end
28
+
29
+ return true
30
+ end
31
+
32
+
33
+ def generate_s3_uri(bucket_name='', object_name='', params=[])
34
+ # Decide between the default and sub-domain host name formats
35
+ if valid_dns_name(bucket_name)
36
+ hostname = bucket_name + "." + S3_ENDPOINT
37
+ else
38
+ hostname = S3_ENDPOINT
39
+ end
40
+
41
+ # Build an initial secure or non-secure URI for the end point.
42
+ request_uri = (@secure_http ? "https://" : "http://") + hostname;
43
+
44
+ # Include the bucket name in the URI except for alternative hostnames
45
+ if hostname == S3_ENDPOINT
46
+ request_uri << '/' + URI.escape(bucket_name) if bucket_name != ''
47
+ end
48
+
49
+ # Add object name component to URI if present
50
+ request_uri << '/' + URI.escape(object_name) if object_name != ''
51
+
52
+ # Add request parameters to the URI. Each item in the params variable
53
+ # is a hash dictionary containing multiple keys.
54
+ query = ""
55
+ params.each do |hash|
56
+ hash.each do |name, value|
57
+ query << '&' if query.length > 0
58
+
59
+ if value.nil?
60
+ query << "#{name}"
61
+ else
62
+ query << "#{name}=#{CGI::escape(value.to_s)}"
63
+ end
64
+ end
65
+ end
66
+ request_uri << "?" + query if query.length > 0
67
+
68
+ return URI.parse(request_uri)
69
+ end
70
+
71
+
72
+ def get_owner_id
73
+ uri = generate_s3_uri()
74
+ response = do_rest('GET', uri)
75
+ buckets = []
76
+
77
+ xml_doc = REXML::Document.new(response.body)
78
+ xml_doc.elements['//Owner/ID'].text
79
+ end
80
+
81
+ def list_buckets
82
+ uri = generate_s3_uri()
83
+ response = do_rest('GET', uri)
84
+ buckets = []
85
+
86
+ xml_doc = REXML::Document.new(response.body)
87
+
88
+ xml_doc.elements.each('//Buckets/Bucket') do |bucket|
89
+ buckets << {
90
+ :name => bucket.elements['Name'].text,
91
+ :creation_date => bucket.elements['CreationDate'].text
92
+ }
93
+ end
94
+
95
+ return {
96
+ :owner_id => xml_doc.elements['//Owner/ID'].text,
97
+ :display_name => xml_doc.elements['//Owner/DisplayName'].text,
98
+ :buckets => buckets
99
+ }
100
+ end
101
+
102
+ def create_bucket(bucket_name, location=nil)
103
+ uri = generate_s3_uri(bucket_name)
104
+
105
+ if location
106
+ xml_doc = REXML::Document.new("<CreateBucketConfiguration/>")
107
+ xml_doc.root.add_attribute('xmlns', XMLNS)
108
+ xml_doc.root.add_element('LocationConstraint').text = location
109
+ do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'text/xml'})
110
+ else
111
+ do_rest('PUT', uri)
112
+ end
113
+
114
+ return true
115
+ end
116
+
117
+ def delete_bucket(bucket_name)
118
+ uri = generate_s3_uri(bucket_name)
119
+ do_rest('DELETE', uri)
120
+ return true
121
+ end
122
+
123
+ def get_bucket_location(bucket_name)
124
+ uri = generate_s3_uri(bucket_name, '', [:location=>nil])
125
+ response = do_rest('GET', uri)
126
+
127
+ xml_doc = REXML::Document.new(response.body)
128
+ return xml_doc.elements['LocationConstraint'].text
129
+ end
130
+
131
+ def list_objects(bucket_name, *params)
132
+ is_truncated = true
133
+
134
+ objects = []
135
+ prefixes = []
136
+
137
+ while is_truncated
138
+ uri = generate_s3_uri(bucket_name, '', params)
139
+ response = do_rest('GET', uri)
140
+
141
+ xml_doc = REXML::Document.new(response.body)
142
+
143
+ xml_doc.elements.each('//Contents') do |contents|
144
+ objects << {
145
+ :key => contents.elements['Key'].text,
146
+ :size => contents.elements['Size'].text,
147
+ :last_modified => contents.elements['LastModified'].text,
148
+ :etag => contents.elements['ETag'].text,
149
+ :owner_id => contents.elements['Owner/ID'].text,
150
+ :owner_name => contents.elements['Owner/DisplayName'].text
151
+ }
152
+ end
153
+
154
+ cps = xml_doc.elements.to_a('//CommonPrefixes')
155
+ if cps.length > 0
156
+ cps.each do |cp|
157
+ prefixes << cp.elements['Prefix'].text
158
+ end
159
+ end
160
+
161
+ # Determine whether listing is truncated
162
+ is_truncated = 'true' == xml_doc.elements['//IsTruncated'].text
163
+
164
+ # Remove any existing marker value
165
+ params.delete_if {|p| p[:marker]}
166
+
167
+ # Set the marker parameter to the NextMarker if possible,
168
+ # otherwise set it to the last key name in the listing
169
+ next_marker_elem = xml_doc.elements['//NextMarker']
170
+ last_key_elem = xml_doc.elements['//Contents/Key[last()]']
171
+
172
+ if next_marker_elem
173
+ params << {:marker => next_marker_elem.text}
174
+ elsif last_key_elem
175
+ params << {:marker => last_key_elem.text}
176
+ else
177
+ params << {:marker => ''}
178
+ end
179
+
180
+ end
181
+
182
+ return {
183
+ :bucket_name => bucket_name,
184
+ :objects => objects,
185
+ :prefixes => prefixes
186
+ }
187
+ end
188
+
189
+
190
+ def create_object(bucket_name, object_key, opts={})
191
+ # Initialize local variables for the provided option items
192
+ data = (opts[:data] ? opts[:data] : '')
193
+ headers = (opts[:headers] ? opts[:headers].clone : {})
194
+ metadata = (opts[:metadata] ? opts[:metadata].clone : {})
195
+
196
+ # The Content-Length header must always be set when data is uploaded.
197
+ headers['Content-Length'] =
198
+ (data.respond_to?(:stat) ? data.stat.size : data.size).to_s
199
+
200
+ # Calculate an md5 hash of the data for upload verification
201
+ if data.respond_to?(:stat)
202
+ # Generate MD5 digest from file data one chunk at a time
203
+ md5_digest = Digest::MD5.new
204
+ File.open(data.path, 'rb') do |io|
205
+ buffer = ''
206
+ md5_digest.update(buffer) while io.read(4096, buffer)
207
+ end
208
+ md5_hash = md5_digest.digest
209
+ else
210
+ md5_hash = Digest::MD5.digest(data)
211
+ end
212
+ headers['Content-MD5'] = encode_base64(md5_hash)
213
+
214
+ # Set the canned policy, may be: 'private', 'public-read',
215
+ # 'public-read-write', 'authenticated-read'
216
+ headers['x-amz-acl'] = opts[:policy] if opts[:policy]
217
+
218
+ # Set an explicit content type if none is provided, otherwise the
219
+ # ruby HTTP library will use its own default type
220
+ # 'application/x-www-form-urlencoded'
221
+ if not headers['Content-Type']
222
+ headers['Content-Type'] =
223
+ data.respond_to?(:to_str) ? 'text/plain' : 'application/octet-stream'
224
+ end
225
+
226
+ # Convert metadata items to headers using the
227
+ # S3 metadata header name prefix.
228
+ metadata.each do |n,v|
229
+ headers["x-amz-meta-#{n}"] = v
230
+ end
231
+
232
+ uri = generate_s3_uri(bucket_name, object_key)
233
+ do_rest('PUT', uri, data, headers)
234
+ return true
235
+ end
236
+
237
+ # The copy object feature was added to the S3 API after the release of
238
+ # "Programming Amazon Web Services" so it is not discussed in the book's
239
+ # text. For more details, see:
240
+ # http://www.jamesmurty.com/2008/05/06/s3-copy-object-in-beta/
241
+ def copy_object(source_bucket_name, source_object_key,
242
+ dest_bucket_name, dest_object_key, acl=nil, new_metadata=nil)
243
+
244
+ headers = {}
245
+
246
+ # Identify the source object
247
+ headers['x-amz-copy-source'] = CGI::escape(
248
+ source_bucket_name + '/' + source_object_key)
249
+
250
+ # Copy metadata from original object, or replace the metadata.
251
+ if new_metadata.nil?
252
+ headers['x-amz-metadata-directive'] = 'COPY'
253
+ else
254
+ headers['x-amz-metadata-directive'] = 'REPLACE'
255
+ headers.merge!(new_metadata)
256
+ end
257
+
258
+ # The Content-Length header must always be set when data is uploaded.
259
+ headers['Content-Length'] = '0'
260
+
261
+ # Set the canned policy, may be: 'private', 'public-read',
262
+ # 'public-read-write', 'authenticated-read'
263
+ headers['x-amz-acl'] = acl if acl
264
+
265
+ uri = generate_s3_uri(dest_bucket_name, dest_object_key)
266
+ do_rest('PUT', uri, nil, headers)
267
+ return true
268
+ end
269
+
270
+ def delete_object(bucket_name, object_key)
271
+ uri = generate_s3_uri(bucket_name, object_key)
272
+ do_rest('DELETE', uri)
273
+ return true
274
+ end
275
+
276
+ def get_object_metadata(bucket_name, object_key, headers={})
277
+ uri = generate_s3_uri(bucket_name, object_key)
278
+ response = do_rest('HEAD', uri, nil, headers)
279
+
280
+ response_headers = {}
281
+ metadata = {}
282
+
283
+ response.each_header do |name,value|
284
+ if name.index('x-amz-meta-') == 0
285
+ metadata[name['x-amz-meta-'.length..-1]] = value
286
+ else
287
+ response_headers[name] = value
288
+ end
289
+ end
290
+
291
+ return {
292
+ :metadata => metadata,
293
+ :headers => response_headers
294
+ }
295
+ end
296
+
297
+ def get_object(bucket_name, object_key, headers={})
298
+ uri = generate_s3_uri(bucket_name, object_key)
299
+
300
+ if block_given?
301
+ response = do_rest('GET', uri, nil, headers) {|segment| yield(segment)}
302
+ else
303
+ response = do_rest('GET', uri, nil, headers)
304
+ end
305
+
306
+ response_headers = {}
307
+ metadata = {}
308
+
309
+ response.each_header do |name,value|
310
+ if name.index('x-amz-meta-') == 0
311
+ metadata[name['x-amz-meta-'.length..-1]] = value
312
+ else
313
+ response_headers[name] = value
314
+ end
315
+ end
316
+
317
+ result = {
318
+ :metadata => metadata,
319
+ :headers => response_headers
320
+ }
321
+ result[:body] = response.body if not block_given?
322
+
323
+ return result
324
+ end
325
+
326
+ def get_logging(bucket_name)
327
+ uri = generate_s3_uri(bucket_name, '', [:logging=>nil])
328
+ response = do_rest('GET', uri)
329
+
330
+ xml_doc = REXML::Document.new(response.body)
331
+
332
+ if xml_doc.elements['//LoggingEnabled']
333
+ return {
334
+ :target_bucket => xml_doc.elements['//TargetBucket'].text,
335
+ :target_prefix => xml_doc.elements['//TargetPrefix'].text
336
+ }
337
+ else
338
+ # Logging is not enabled
339
+ return nil
340
+ end
341
+ end
342
+
343
+ def set_logging(bucket_name, target_bucket=nil,
344
+ target_prefix="#{bucket_name}.")
345
+
346
+ # Build BucketLoggingStatus XML document
347
+ xml_doc = REXML::Document.new("<BucketLoggingStatus xmlns='#{XMLNS}'/>")
348
+
349
+ if target_bucket
350
+ logging_enabled = xml_doc.root.add_element('LoggingEnabled')
351
+ logging_enabled.add_element('TargetBucket').text = target_bucket
352
+ logging_enabled.add_element('TargetPrefix').text = target_prefix
353
+ end
354
+
355
+ uri = generate_s3_uri(bucket_name, '', [:logging=>nil])
356
+ do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'application/xml'})
357
+ return true
358
+ end
359
+
360
+ def get_acl(bucket_name, object_key='')
361
+ uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
362
+ response = do_rest('GET', uri)
363
+
364
+ xml_doc = REXML::Document.new(response.body)
365
+
366
+ grants = []
367
+
368
+ xml_doc.elements.each('//Grant') do |grant|
369
+ grantee = {}
370
+
371
+ grantee[:type] = grant.elements['Grantee'].attributes['type']
372
+
373
+ if grantee[:type] == 'Group'
374
+ grantee[:uri] = grant.elements['Grantee/URI'].text
375
+ else
376
+ grantee[:id] = grant.elements['Grantee/ID'].text
377
+ grantee[:display_name] = grant.elements['Grantee/DisplayName'].text
378
+ end
379
+
380
+ grants << {
381
+ :grantee => grantee,
382
+ :permission => grant.elements['Permission'].text
383
+ }
384
+ end
385
+
386
+ return {
387
+ :owner_id => xml_doc.elements['//Owner/ID'].text,
388
+ :owner_name => xml_doc.elements['//Owner/DisplayName'].text,
389
+ :grants => grants
390
+ }
391
+ end
392
+
393
+ def set_acl(owner_id, bucket_name, object_key='',
394
+ grants=[owner_id=>'FULL_CONTROL'])
395
+
396
+ xml_doc = REXML::Document.new("<AccessControlPolicy xmlns='#{XMLNS}'/>")
397
+ xml_doc.root.add_element('Owner').add_element('ID').text = owner_id
398
+ grant_list = xml_doc.root.add_element('AccessControlList')
399
+
400
+ grants.each do |hash|
401
+ hash.each do |grantee_id, permission|
402
+
403
+ grant = grant_list.add_element('Grant')
404
+ grant.add_element('Permission').text = permission
405
+
406
+ # Grantee may be of type email, group, or canonical user
407
+ if grantee_id.index('@')
408
+ # Email grantee
409
+ grantee = grant.add_element('Grantee',
410
+ {'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
411
+ 'xsi:type'=>'AmazonCustomerByEmail'})
412
+ grantee.add_element('EmailAddress').text = grantee_id
413
+ elsif grantee_id.index('://')
414
+ # Group grantee
415
+ grantee = grant.add_element('Grantee',
416
+ {'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
417
+ 'xsi:type'=>'Group'})
418
+ grantee.add_element('URI').text = grantee_id
419
+ else
420
+ # Canonical user grantee
421
+ grantee = grant.add_element('Grantee',
422
+ {'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
423
+ 'xsi:type'=>'CanonicalUser'})
424
+ grantee.add_element('ID').text = grantee_id
425
+ end
426
+ end
427
+ end
428
+
429
+ uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
430
+ do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'application/xml'})
431
+ return true
432
+ end
433
+
434
+ def set_canned_acl(canned_acl, bucket_name, object_key='')
435
+ uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
436
+ response = do_rest('PUT', uri, nil, {'x-amz-acl'=>canned_acl})
437
+ return true
438
+ end
439
+
440
+ def get_torrent(bucket_name, object_key, output)
441
+ uri = generate_s3_uri(bucket_name, object_key, [:torrent=>nil])
442
+ response = do_rest('GET', uri)
443
+ output.write(response.body)
444
+ end
445
+
446
+ def sign_uri(method, expires, bucket_name, object_key='', opts={})
447
+ parameters = opts[:parameters] || []
448
+ headers = opts[:headers] || {}
449
+
450
+ headers['Date'] = expires
451
+
452
+ uri = generate_s3_uri(bucket_name, object_key, parameters)
453
+ signature = generate_rest_signature(method, uri, headers)
454
+
455
+ uri.query = (uri.query.nil? ? '' : "#{uri.query}&")
456
+ uri.query << "Signature=" + CGI::escape(signature)
457
+ uri.query << "&Expires=" + expires.to_s
458
+ uri.query << "&AWSAccessKeyId=" + @aws_access_key
459
+
460
+ uri.host = bucket_name if opts[:is_virtual_host]
461
+
462
+ return uri.to_s
463
+ end
464
+
465
+
466
+ def build_post_policy(expiration_time, conditions)
467
+ if expiration_time.nil? or not expiration_time.respond_to?(:getutc)
468
+ raise 'Policy document must include a valid expiration Time object'
469
+ end
470
+ if conditions.nil? or not conditions.class == Hash
471
+ raise 'Policy document must include a valid conditions Hash object'
472
+ end
473
+
474
+ # Convert conditions object mappings to condition statements
475
+ conds = []
476
+ conditions.each_pair do |name,test|
477
+ if test.nil?
478
+ # A nil condition value means allow anything.
479
+ conds << %{["starts-with", "$#{name}", ""]}
480
+ elsif test.is_a? String
481
+ conds << %{{"#{name}": "#{test}"}}
482
+ elsif test.is_a? Array
483
+ conds << %{{"#{name}": "#{test.join(',')}"}}
484
+ elsif test.is_a? Hash
485
+ operation = test[:op]
486
+ value = test[:value]
487
+ conds << %{["#{operation}", "$#{name}", "#{value}"]}
488
+ elsif test.is_a? Range
489
+ conds << %{["#{name}", #{test.begin}, #{test.end}]}
490
+ else
491
+ raise "Unexpected value type for condition '#{name}': #{test.class}"
492
+ end
493
+ end
494
+
495
+ return %{{"expiration": "#{expiration_time.getutc.iso8601}",
496
+ "conditions": [#{conds.join(",")}]}}
497
+ end
498
+
499
+ def build_post_form(bucket_name, key, options={})
500
+ fields = []
501
+
502
+ # Form is only authenticated if a policy is specified.
503
+ if options[:expiration] or options[:conditions]
504
+ # Generate policy document
505
+ policy = build_post_policy(options[:expiration], options[:conditions])
506
+ puts "POST Policy\n===========\n#{policy}\n\n" if @debug
507
+
508
+ # Add the base64-encoded policy document as the 'policy' field
509
+ policy_b64 = encode_base64(policy)
510
+ fields << %{<input type="hidden" name="policy" value="#{policy_b64}">}
511
+
512
+ # Add the AWS access key as the 'AWSAccessKeyId' field
513
+ fields << %{<input type="hidden" name="AWSAccessKeyId"
514
+ value="#{@aws_access_key}">}
515
+
516
+ # Add signature for encoded policy document as the 'AWSAccessKeyId' field
517
+ signature = generate_signature(policy_b64)
518
+ fields << %{<input type="hidden" name="signature" value="#{signature}">}
519
+ end
520
+
521
+ # Include any additional fields
522
+ options[:fields].each_pair do |n,v|
523
+ if v.nil?
524
+ # Allow users to provide their own <input> fields as text.
525
+ fields << n
526
+ else
527
+ fields << %{<input type="hidden" name="#{n}" value="#{v}">}
528
+ end
529
+ end if options[:fields]
530
+
531
+ # Add the vital 'file' input item, which may be a textarea or file.
532
+ if options[:text_input]
533
+ # Use the text_input option which should specify a textarea or text
534
+ # input field. For example:
535
+ # '<textarea name="file" cols="80" rows="5">Default Text</textarea>'
536
+ fields << options[:text_input]
537
+ else
538
+ fields << %{<input name="file" type="file">}
539
+ end
540
+
541
+ # Construct a sub-domain URL to refer to the target bucket. The
542
+ # HTTPS protocol will be used if the secure HTTP option is enabled.
543
+ url = "http#{@secure_http ? 's' : ''}://#{bucket_name}.s3.amazonaws.com/"
544
+
545
+ # Construct the entire form.
546
+ form = %{
547
+ <form action="#{url}" method="post" enctype="multipart/form-data">
548
+ <input type="hidden" name="key" value="#{key}">
549
+ #{fields.join("\n")}
550
+ <br>
551
+ <input type="submit" value="Upload to Amazon S3">
552
+ </form>
553
+ }
554
+ puts "POST Form\n=========\n#{form}\n" if @debug
555
+
556
+ return form
557
+ end
558
+
559
+ end