staugaard-cloudmaster 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,475 @@
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 AWS module includes HTTP messaging and utility methods that handle
7
+ # communication with Amazon Web Services' REST or Query APIs. Service
8
+ # client implementations are built on top of this module.
9
+
10
+ require 'openssl'
11
+ require 'digest/sha1'
12
+ require 'base64'
13
+ require 'cgi'
14
+ require 'net/https'
15
+ require 'time'
16
+ require 'uri'
17
+ require 'rexml/document'
18
+
19
+ $KCODE = 'u' # Enable Unicode (international character) support
20
+
21
+ module AWS
22
+ # Your Amazon Web Services Access Key credential.
23
+ attr_accessor :aws_access_key
24
+
25
+ # Your Amazon Web Services Secret Key credential.
26
+ attr_accessor :aws_secret_key
27
+
28
+ # Use only the Secure HTTP protocol (HTTPS)? When this value is true, all
29
+ # requests are sent using HTTPS. When this value is false, standard HTTP
30
+ # is used.
31
+ attr_accessor :secure_http
32
+
33
+ # Enable debugging messages? When this value is true, debug logging
34
+ # messages describing AWS communication messages are printed to standard
35
+ # output.
36
+ attr_accessor :debug
37
+
38
+ # The approximate difference in the current time between your computer and
39
+ # Amazon's servers, measured in seconds.
40
+ #
41
+ # This value is 0 by default. Use the current_time method to obtain the
42
+ # current time with this offset factor included, and the adjust_time
43
+ # method to calculate an offset value for your computer based on a
44
+ # response from an AWS server.
45
+ attr_reader :time_offset
46
+
47
+ #def initialize(access_key, secret_key, secure_http=true, debug=false)
48
+ # @aws_access_key = access_key
49
+ # @aws_secret_key = secret_key
50
+ # @time_offset = 0
51
+ # @secure_http = secure_http
52
+ # @debug = debug
53
+ #end
54
+
55
+ # Hard-coded credentials
56
+ #def initialize(secure_http=true, debug=false)
57
+ # @aws_access_key = 'YOUR_AWS_ACCESS_KEY'
58
+ # @aws_secret_key = 'YOUR_AWS_SECRET_KEY'
59
+ # @time_offset = 0
60
+ # @secure_http = secure_http
61
+ # @debug = debug
62
+ #end
63
+
64
+ # Initialize AWS and set the service-specific variables: aws_access_key,
65
+ # aws_secret_key, debug, and secure_http.
66
+ def initialize(aws_access_key=ENV['AWS_ACCESS_KEY'],
67
+ aws_secret_key=ENV['AWS_SECRET_KEY'],
68
+ secure_http=true, debug=false)
69
+ @aws_access_key = aws_access_key
70
+ @aws_secret_key = aws_secret_key
71
+ @time_offset = 0
72
+ @secure_http = secure_http
73
+ @debug = debug
74
+ end
75
+
76
+
77
+ # An exception object that captures information about an AWS service error.
78
+ class ServiceError < RuntimeError
79
+ attr_accessor :response, :aws_error_xml
80
+
81
+ # Initialize a ServiceError object based on an HTTP Response
82
+ def initialize(http_response)
83
+ # Store the HTTP response as a class variable
84
+ @response = http_response
85
+
86
+ # Add the HTTP status code and message to a descriptive message
87
+ message = "HTTP Error: #{@response.code} - #{@response.message}"
88
+
89
+ # If an AWS error message is available, add its code and message
90
+ # to the overall descriptive message
91
+ if @response.body and @response.body.index('<?xml') == 0
92
+ @aws_error_xml = REXML::Document.new(@response.body)
93
+
94
+ aws_error_code = @aws_error_xml.elements['//Code'].text
95
+ aws_error_message = @aws_error_xml.elements['//Message'].text
96
+
97
+ message += ", AWS Error: #{aws_error_code} - #{aws_error_message}"
98
+ end
99
+
100
+ # Initialize the RuntimeError superclass with the descriptive message
101
+ super(message)
102
+ end
103
+
104
+ end
105
+
106
+
107
+ # Generates an AWS signature value for the given request description.
108
+ # The result value is a HMAC signature that is cryptographically signed
109
+ # with the SHA1 algorithm using your AWS Secret Key credential. The
110
+ # signature value is Base64 encoded before being returned.
111
+ #
112
+ # This method can be used to sign requests destined for the REST or
113
+ # Query AWS API interfaces.
114
+ def generate_signature(request_description)
115
+ raise "aws_access_key is not set" if not @aws_access_key
116
+ raise "aws_secret_key is not set" if not @aws_secret_key
117
+
118
+ digest_generator = OpenSSL::Digest::Digest.new('sha1')
119
+ digest = OpenSSL::HMAC.digest(digest_generator,
120
+ @aws_secret_key,
121
+ request_description)
122
+ b64_sig = encode_base64(digest)
123
+ return b64_sig
124
+ end
125
+
126
+
127
+ # Converts a minimal set of parameters destined for an AWS Query API
128
+ # interface into a complete set necessary for invoking an AWS operation.
129
+ #
130
+ # Normal parameters are included in the resultant complete set as-is.
131
+ #
132
+ # Indexed parameters are converted into multiple parameter name/value
133
+ # pairs, where the name starts with the given parameter name but has a
134
+ # suffix value appended to it. For example, the input mapping
135
+ # 'Name' => ['x','y']
136
+ # will be converted to two parameters:
137
+ # 'Name.1' => 'x'
138
+ # 'Name.2' => 'y'
139
+ def build_query_params(api_ver, sig_ver, params, indexed_params={}, indexed_start=1)
140
+ # Set mandatory query parameters
141
+ built_params = {
142
+ 'Version' => api_ver,
143
+ 'SignatureVersion' => sig_ver,
144
+ 'AWSAccessKeyId' => @aws_access_key
145
+ }
146
+
147
+ # Use current time as timestamp if no date/time value is already set
148
+ if params['Timestamp'].nil? and params['Expires'].nil?
149
+ params['Timestamp'] = current_time.getutc.iso8601
150
+ end
151
+
152
+ # Merge parameters provided with defaults after removing
153
+ # any parameters without a value.
154
+ built_params.merge!(params.reject {|name,value| value.nil?})
155
+
156
+ # Add any indexed parameters as ParamName.1, ParamName.2, etc
157
+ indexed_params.each do |param_name,value_array|
158
+ index_count = indexed_start
159
+ value_array.each do |value|
160
+ built_params["#{param_name}.#{index_count}"] = value
161
+ index_count += 1
162
+ end if value_array
163
+ end
164
+
165
+ return built_params
166
+ end
167
+
168
+
169
+ # Sends a GET or POST request message to an AWS service's Query API
170
+ # interface and returns the response result from the service. This method
171
+ # signs the request message with your AWS credentials.
172
+ #
173
+ # If the AWS service returns an error message, this method will throw a
174
+ # ServiceException describing the error.
175
+ def do_query(method, uri, parameters)
176
+ # Ensure the URI is using Secure HTTP protocol if the flag is set
177
+ if @secure_http
178
+ uri.scheme = 'https'
179
+ uri.port = 443
180
+ else
181
+ uri.scheme = 'http'
182
+ uri.port = 80
183
+ end
184
+
185
+ # Generate request description and signature, and add to the request
186
+ # as the parameter 'Signature'
187
+ req_desc = parameters.sort {|x,y| x[0].downcase <=> y[0].downcase}.to_s
188
+ signature = generate_signature(req_desc)
189
+ parameters['Signature'] = signature
190
+
191
+ case method
192
+ when 'GET'
193
+ # Create GET request with parameters in URI
194
+ uri.query = ''
195
+ parameters.each do |name, value|
196
+ uri.query << "#{name}=#{CGI::escape(value.to_s)}&"
197
+ end
198
+ req = Net::HTTP::Get.new(uri.request_uri)
199
+ when 'POST'
200
+ # Create POST request with parameters in form data
201
+ req = Net::HTTP::Post.new(uri.request_uri)
202
+ req.set_form_data(parameters)
203
+ req.set_content_type('application/x-www-form-urlencoded',
204
+ {'charset', 'utf-8'})
205
+ else
206
+ raise "Invalid HTTP Query method: #{method}"
207
+ end
208
+
209
+ # Setup HTTP connection, optionally with SSL security
210
+ Net::HTTP.version_1_1
211
+ http = Net::HTTP.new(uri.host, uri.port)
212
+ if @secure_http
213
+ http.use_ssl = true
214
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
215
+ end
216
+
217
+ debug_request(method, uri, {}, parameters) if @debug
218
+
219
+ response = http.request(req)
220
+
221
+ debug_response(response) if @debug
222
+
223
+ if response.is_a?(Net::HTTPSuccess)
224
+ return response
225
+ else
226
+ raise ServiceError.new(response)
227
+ end
228
+ end
229
+
230
+
231
+ # Generates a request description string for a request destined for a REST
232
+ # AWS API interface, and returns a signature value for the request.
233
+ #
234
+ # This method will work for any REST AWS request, though it is intended
235
+ # mainly for the S3 service's API and handles special cases required for
236
+ # this service.
237
+ def generate_rest_signature(method, uri, headers)
238
+ # Set mandatory Date header if it is missing
239
+ headers['Date'] = current_time.httpdate if not headers['Date']
240
+
241
+ # Describe main components of REST request
242
+ req_desc =
243
+ "#{method}\n" +
244
+ "#{headers['Content-MD5']}\n" +
245
+ "#{headers['Content-Type']}\n" +
246
+ "#{headers['Date']}\n"
247
+
248
+ # Find any x-amz-* headers, sort them and append to the description
249
+ amz_headers = headers.reject {|name,value| name.index('x-amz-') != 0}
250
+ amz_headers = amz_headers.sort {|x, y| x[0] <=> y[0]}
251
+ amz_headers.each {|name,value| req_desc << "#{name.downcase}:#{value}\n"}
252
+
253
+ path = ''
254
+
255
+ # Handle special case of S3 alternative hostname URIs. The bucket
256
+ # portion of alternative hostnames must be included in the request
257
+ # description's URI path.
258
+ if not ['s3.amazonaws.com', 'queue.amazonaws.com'].include?(uri.host)
259
+ if uri.host =~ /(.*).s3.amazonaws.com/
260
+ path << '/' + $1
261
+ else
262
+ path << '/' + uri.host
263
+ end
264
+ # For alternative hosts, the path must end with a slash if there is
265
+ # no object in the path.
266
+ path << '/' if uri.path == ''
267
+ end
268
+
269
+ # Append the request's URI path to the description
270
+ path << uri.path
271
+
272
+ # Ensure the request description's URI path includes at least a slash.
273
+ if path == ''
274
+ req_desc << '/'
275
+ else
276
+ req_desc << path
277
+ end
278
+
279
+ # Append special S3 parameters to request description
280
+ if uri.query
281
+ uri.query.split('&').each do |param|
282
+ if ['acl', 'torrent', 'logging', 'location'].include?(param)
283
+ req_desc << "?" + param
284
+ end
285
+ end
286
+ end
287
+
288
+ if @debug
289
+ puts "REQUEST DESCRIPTION\n======="
290
+ puts "#{req_desc.gsub("\n","\\n\n")}\n\n"
291
+ end
292
+
293
+ # Generate signature
294
+ return generate_signature(req_desc)
295
+ end
296
+
297
+
298
+ # Sends a GET, HEAD, DELETE or PUT request message to an AWS service's
299
+ # REST API interface and returns the response result from the service. This
300
+ # method signs the request message with your AWS credentials.
301
+ #
302
+ # If the AWS service returns an error message, this method will throw a
303
+ # ServiceException describing the error. This method also includes support
304
+ # for following Temporary Redirect responses (with HTTP response
305
+ # codes 307).
306
+ def do_rest(method, uri, data=nil, headers={})
307
+ # Generate request description and signature, and add to the request
308
+ # as the header 'Authorization'
309
+ signature = generate_rest_signature(method, uri, headers)
310
+ headers['Authorization'] = "AWS #{@aws_access_key}:#{signature}"
311
+
312
+ # Ensure the Host header is always set
313
+ headers['Host'] = uri.host
314
+
315
+ # Tell service to confirm the request message is valid before it
316
+ # accepts data. Confirmation is indicated by a 100 (Continue) message
317
+ headers['Expect'] = '100-continue' if method == 'PUT'
318
+
319
+ redirect_count = 0
320
+ while redirect_count < 5 # Repeat requests after a 307 Temporary Redirect
321
+
322
+ # Setup a new HTTP connection, optionally with secure HTTPS enabled
323
+ Net::HTTP.version_1_1
324
+ http = Net::HTTP.new(uri.host, uri.port)
325
+ if @secure_http
326
+ http.use_ssl = true
327
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
328
+ end
329
+
330
+ debug_request(method, uri, headers, {}, data) if @debug
331
+
332
+ # Perform the request. Uploads via the PUT method get special treatment
333
+ if method == 'PUT'
334
+ if data.respond_to?(:stat)
335
+ # Special case for file uploads, these are streamed
336
+ req = Net::HTTP::Put.new(uri.path, headers)
337
+ req.body_stream=data
338
+ response = http.request(req)
339
+ else
340
+ # Ensure HTTP content-length header is set to correct value
341
+ headers['Content-Length'] = (data.nil? ? '0' : data.length.to_s)
342
+ response = http.send_request(method, uri.request_uri, data, headers)
343
+ end
344
+ elsif method == 'GET' and block_given?
345
+ # Special case for streamed downloads
346
+ http.request_get(uri.request_uri, headers) do |response|
347
+ response.read_body {|segment| yield(segment)}
348
+ end
349
+ else
350
+ response = http.send_request(method, uri.request_uri, data, headers)
351
+ end
352
+
353
+ debug_response(response) if @debug
354
+
355
+ if response.is_a?(Net::HTTPTemporaryRedirect) # 307 Redirect
356
+ # Update the request to use the temporary redirect URI location
357
+ uri = URI.parse(response.header['location'])
358
+ redirect_count += 1 # Count to prevent infinite redirects
359
+ elsif response.is_a?(Net::HTTPSuccess)
360
+ return response
361
+ else
362
+ raise ServiceError.new(response)
363
+ end
364
+
365
+ end # End of while loop
366
+ end
367
+
368
+
369
+ # Prints detailed information about an HTTP request message to standard
370
+ # output.
371
+ def debug_request(method, uri, headers={}, query_parameters={}, data=nil)
372
+ puts "REQUEST\n======="
373
+ puts "Method : #{method}"
374
+
375
+ # Print URI
376
+ params = uri.to_s.split('&')
377
+ puts "URI : #{params.first}"
378
+ params[1..-1].each {|p| puts "\t &#{p}"} if params.length > 1
379
+
380
+ # Print Headers
381
+ if headers.length > 0
382
+ puts "Headers:"
383
+ headers.each {|n,v| puts " #{n}=#{v}"}
384
+ end
385
+
386
+ # Print Query Parameters
387
+ if query_parameters.length > 0
388
+ puts "Query Parameters:"
389
+ query_parameters.each {|n,v| puts " #{n}=#{v}"}
390
+ end
391
+
392
+ # Print Request Data
393
+ if data
394
+ puts "Request Body Data:"
395
+ if headers['Content-Type'] == 'application/xml'
396
+ # Pretty-print XML data
397
+ REXML::Document.new(data).write($stdout, 2)
398
+ else
399
+ puts data
400
+ end
401
+ data.rewind if data.respond_to?(:stat)
402
+ puts
403
+ end
404
+ end
405
+
406
+
407
+ # Prints detailed information about an HTTP response message to standard
408
+ # output.
409
+ def debug_response(response)
410
+ puts "\nRESPONSE\n========"
411
+ puts "Status : #{response.code} #{response.message}"
412
+
413
+ # Print Headers
414
+ if response.header.length > 0
415
+ puts "Headers:"
416
+ response.header.each {|n,v| puts " #{n}=#{v}"}
417
+ end
418
+
419
+ if response.body and response.body.respond_to?(:length)
420
+ puts "Body:"
421
+ if response.body.index('<?xml') == 0
422
+ # Pretty-print XML data
423
+ REXML::Document.new(response.body).write($stdout)
424
+ else
425
+ puts response.body
426
+ end
427
+ end
428
+ puts
429
+ end
430
+
431
+
432
+ # Returns the current date and time, adjusted according to the time
433
+ # offset between your computer and an AWS server (as set by the
434
+ # adjust_time method.
435
+ def current_time
436
+ if @time_offset
437
+ return Time.now + @time_offset
438
+ else
439
+ return Time.now
440
+ end
441
+ end
442
+
443
+
444
+ # Sets a time offset value to reflect the time difference between your
445
+ # computer's clock and the current time according to an AWS server. This
446
+ # method returns the calculated time difference and also sets the
447
+ # timeOffset variable in AWS.
448
+ #
449
+ # Ideally you should not rely on this method to overcome clock-related
450
+ # disagreements between your computer and AWS. If you computer is set
451
+ # to update its clock periodically and has the correct timezone setting
452
+ # you should never have to resort to this work-around.
453
+ def adjust_time(uri=URI.parse('http://aws.amazon.com/'))
454
+ http = Net::HTTP.new(uri.host, uri.port)
455
+ response = http.send_request('GET', uri.request_uri)
456
+
457
+ local_time = Time.new
458
+ aws_time = Time.httpdate(response.header['Date'])
459
+ @time_offset = aws_time - local_time
460
+
461
+ puts "Time offset for AWS requests: #{@time_offset} seconds" if @debug
462
+ return @time_offset
463
+ end
464
+
465
+
466
+ # Base64-encodes a string, and removes the excess newline ('\n')
467
+ # characters inserted by the default ruby encoder.
468
+ def encode_base64(data)
469
+ return nil if not data
470
+ b64 = Base64.encode64(data)
471
+ cleaned = b64.gsub("\n","")
472
+ return cleaned
473
+ end
474
+
475
+ end