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,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