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