right_support 2.8.3 → 2.8.6
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.
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/VERSION +1 -1
- data/features/serialization.feature +8 -0
- data/lib/right_support/data/serializer.rb +2 -0
- data/lib/right_support/db/cassandra_model.rb +131 -61
- data/lib/right_support/net/dns.rb +147 -46
- data/lib/right_support/net/http_client.rb +4 -0
- data/lib/right_support/net/request_balancer.rb +2 -2
- data/right_support.gemspec +8 -9
- data/spec/db/cassandra_model_part1_spec.rb +3 -3
- data/spec/net/dns_spec.rb +45 -10
- data/spec/net/request_balancer_spec.rb +10 -6
- data/spec/stats/helpers_spec.rb +1 -1
- metadata +24 -26
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -34,6 +34,7 @@ GEM
|
|
34
34
|
gherkin (2.12.0)
|
35
35
|
multi_json (~> 1.3)
|
36
36
|
git (1.2.5)
|
37
|
+
iconv (1.0.4)
|
37
38
|
jeweler (1.8.4)
|
38
39
|
bundler (~> 1.0)
|
39
40
|
git (>= 1.2.5)
|
@@ -97,6 +98,7 @@ PLATFORMS
|
|
97
98
|
DEPENDENCIES
|
98
99
|
addressable (~> 2.2.7)
|
99
100
|
flexmock (~> 1.0)
|
101
|
+
iconv
|
100
102
|
jeweler (~> 1.8.3)
|
101
103
|
net-ssh (~> 2.0)
|
102
104
|
nokogiri (~> 1.5)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.8.
|
1
|
+
2.8.6
|
@@ -31,6 +31,14 @@ Feature: JSON serialization
|
|
31
31
|
When I serialize a complex random data structure
|
32
32
|
Then the serialized value should round-trip cleanly
|
33
33
|
|
34
|
+
Scenario Outline: Ruby string with non UTF-8 characters
|
35
|
+
When I serialize the Ruby value: <ruby>
|
36
|
+
Then the serialized value should be: <json>
|
37
|
+
|
38
|
+
Examples:
|
39
|
+
| ruby | json |
|
40
|
+
| "\xC0\xBCscript>\xC0\xBC/script>" | "script>/script>" |
|
41
|
+
|
34
42
|
Scenario Outline: object-escaped Symbol
|
35
43
|
When I serialize the Ruby value: <ruby>
|
36
44
|
Then the serialized value should be: <json>
|
@@ -145,8 +145,10 @@ module RightSupport::Data
|
|
145
145
|
# @param [Object] object any Ruby object
|
146
146
|
# @return [Object] JSONish representation of input object
|
147
147
|
def object_to_jsonish(object)
|
148
|
+
iconv = Iconv.new('UTF-8//IGNORE', 'UTF-8')
|
148
149
|
case object
|
149
150
|
when String
|
151
|
+
object = iconv.iconv(object)
|
150
152
|
if (object =~ /^:/ ) || (object =~ TIME_PATTERN)
|
151
153
|
# Strings that look like a Symbol or Time must be object-escaped.
|
152
154
|
{@marker => String.name,
|
@@ -44,6 +44,40 @@ if require_succeeds?('cassandra/0.8')
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
# Monkey patch get_range_single so that it can return a array of key slices
|
48
|
+
# rather than converting it to a Hash
|
49
|
+
def get_range_single(column_family, options = {})
|
50
|
+
return_empty_rows = options.delete(:return_empty_rows) || false
|
51
|
+
slices_please = options.delete(:slices_not_hash) || false
|
52
|
+
|
53
|
+
column_family, _, _, options =
|
54
|
+
extract_and_validate_params(column_family, "", [options],
|
55
|
+
READ_DEFAULTS.merge(:start_key => '',
|
56
|
+
:finish_key => '',
|
57
|
+
:key_count => 100,
|
58
|
+
:columns => nil,
|
59
|
+
:reversed => false
|
60
|
+
)
|
61
|
+
)
|
62
|
+
|
63
|
+
results = _get_range( column_family,
|
64
|
+
options[:start_key].to_s,
|
65
|
+
options[:finish_key].to_s,
|
66
|
+
options[:key_count],
|
67
|
+
options[:columns],
|
68
|
+
options[:start].to_s,
|
69
|
+
options[:finish].to_s,
|
70
|
+
options[:count],
|
71
|
+
options[:consistency],
|
72
|
+
options[:reversed] )
|
73
|
+
|
74
|
+
unless slices_please
|
75
|
+
multi_key_slices_to_hash(column_family, results, return_empty_rows)
|
76
|
+
else
|
77
|
+
results
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
47
81
|
# Monkey patch get_indexed_slices so that it returns OrderedHash, otherwise cannot determine
|
48
82
|
# next start key when getting in chunks
|
49
83
|
def get_indexed_slices(column_family, index_clause, *columns_and_options)
|
@@ -124,6 +158,16 @@ module RightSupport::DB
|
|
124
158
|
|
125
159
|
METHODS_TO_LOG = [:multi_get, :get, :get_indexed_slices, :get_columns, :insert, :remove, 'multi_get', 'get', 'get_indexed_slices', 'get_columns', 'insert', 'remove']
|
126
160
|
|
161
|
+
#This is required to be overwritten in order to set the read CL
|
162
|
+
def default_read_consistency
|
163
|
+
nil
|
164
|
+
end
|
165
|
+
|
166
|
+
#This is required to be overwritten in order to set the write CL
|
167
|
+
def default_write_consistency
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
127
171
|
# Deprecate usage of CassandraModel under Ruby < 1.9
|
128
172
|
def inherited(base)
|
129
173
|
raise UnsupportedRubyVersion, "Support only Ruby >= 1.9" unless RUBY_VERSION >= "1.9"
|
@@ -229,6 +273,23 @@ module RightSupport::DB
|
|
229
273
|
@@current_keyspace = nil
|
230
274
|
end
|
231
275
|
|
276
|
+
def get_connection(current=nil)
|
277
|
+
config = env_config
|
278
|
+
thrift_client_options = {:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT}
|
279
|
+
thrift_client_options = {
|
280
|
+
:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT,
|
281
|
+
:server_retry_period => nil,
|
282
|
+
}
|
283
|
+
|
284
|
+
thrift_client_options.merge!({:protocol => Thrift::BinaryProtocolAccelerated}) if defined? Thrift::BinaryProtocolAccelerated
|
285
|
+
|
286
|
+
current ||= Cassandra.new(keyspace, config["server"], thrift_client_options)
|
287
|
+
current.disable_node_auto_discovery!
|
288
|
+
current.default_write_consistency = self.default_write_consistency if self.default_write_consistency
|
289
|
+
current.default_read_consistency = self.default_read_consistency if self.default_read_consistency
|
290
|
+
current
|
291
|
+
end
|
292
|
+
|
232
293
|
# Client connected to Cassandra server
|
233
294
|
# Create connection if does not already exist
|
234
295
|
# Use BinaryProtocolAccelerated if it available
|
@@ -237,20 +298,7 @@ module RightSupport::DB
|
|
237
298
|
# (Cassandra):: Client connected to server
|
238
299
|
def conn()
|
239
300
|
@@connections ||= {}
|
240
|
-
|
241
|
-
config = env_config
|
242
|
-
|
243
|
-
thrift_client_options = {
|
244
|
-
:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT,
|
245
|
-
:server_retry_period => nil,
|
246
|
-
}
|
247
|
-
|
248
|
-
if defined? Thrift::BinaryProtocolAccelerated
|
249
|
-
thrift_client_options.merge!({:protocol => Thrift::BinaryProtocolAccelerated})
|
250
|
-
end
|
251
|
-
|
252
|
-
@@connections[self.keyspace] ||= Cassandra.new(self.keyspace, config["server"], thrift_client_options)
|
253
|
-
@@connections[self.keyspace].disable_node_auto_discovery!
|
301
|
+
@@connections[self.keyspace] = get_connection(@@connections[self.keyspace])
|
254
302
|
@@connections[self.keyspace]
|
255
303
|
end
|
256
304
|
|
@@ -371,48 +419,79 @@ module RightSupport::DB
|
|
371
419
|
def stream_all_indexed_slices(index, key)
|
372
420
|
expr = do_op(:create_idx_expr, index, key, "EQ")
|
373
421
|
|
374
|
-
start_row
|
375
|
-
|
376
|
-
|
422
|
+
start_row = ''
|
423
|
+
max_row_count = 100
|
424
|
+
max_initial_column_count = 1000 # number of columns to retrieve in the initial 2ndary index search
|
425
|
+
max_additional_column_count = 10000 # Number of columns to retrieve in a batch once we're targetting a single (long) row
|
377
426
|
|
427
|
+
# Loop over all CF rows, with batches of X
|
378
428
|
while (start_row != nil)
|
379
|
-
clause = do_op(:create_idx_clause, [expr], start_row,
|
380
|
-
|
381
|
-
rows
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
429
|
+
clause = do_op(:create_idx_clause, [expr], start_row, max_row_count)
|
430
|
+
|
431
|
+
# Now, for each batch of rows, make sure don't ask for "ALL" columns of each row, to avoid hitting rows with a huge amount of columns,
|
432
|
+
# which would cause large memory pressure here in the client, but more specially might cause long wait times and possible timeouts.
|
433
|
+
begin
|
434
|
+
rows = self.conn.get_indexed_slices(column_family, clause, :count => max_initial_column_count)
|
435
|
+
rescue Exception => e
|
436
|
+
wrapped_timeout = e.is_a?(CassandraThrift::TimedOutException)
|
437
|
+
unwrapped_timeout = e.is_a?(Thrift::TransportException) && (e.type == Thrift::TransportException::TIMED_OUT)
|
438
|
+
unwrapped_disconnect = e.is_a?(Thrift::TransportException) && (e.type == Thrift::TransportException::NOT_OPEN)
|
439
|
+
|
440
|
+
if (wrapped_timeout || unwrapped_timeout || unwrapped_disconnect) && (timeout_retries < retry_timeout)
|
441
|
+
timeout_retries += 1
|
442
|
+
retry
|
443
|
+
else
|
444
|
+
timeout_retries = 0
|
445
|
+
raise e
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
rows.each_pair do |row_key, columns|
|
450
|
+
# We already processed this row the previous iteration
|
451
|
+
next if row_key == start_row
|
452
|
+
|
453
|
+
yield(row_key, columns)
|
454
|
+
|
455
|
+
if columns.size >= max_initial_column_count
|
456
|
+
# Loop over all columns of the row (1000 at a time) starting at the last column name
|
457
|
+
last_column_name = columns.last.column.name
|
458
|
+
while( last_column_name != nil )
|
459
|
+
begin
|
460
|
+
# Retrieve a slice of this row excluding the first column
|
461
|
+
# as it's already been processed.
|
462
|
+
more_cols = self.conn.get_range(
|
463
|
+
column_family,
|
464
|
+
:start_key => row_key,
|
465
|
+
:finish_key => row_key,
|
466
|
+
:count => max_additional_column_count,
|
467
|
+
:start => last_column_name,
|
468
|
+
:slices_not_hash => true ).first.columns[1..-1]
|
469
|
+
rescue Exception => e
|
470
|
+
wrapped_timeout = e.is_a?(CassandraThrift::TimedOutException)
|
471
|
+
unwrapped_timeout = e.is_a?(Thrift::TransportException) && (e.type == Thrift::TransportException::TIMED_OUT)
|
472
|
+
unwrapped_disconnect = e.is_a?(Thrift::TransportException) && (e.type == Thrift::TransportException::NOT_OPEN)
|
473
|
+
|
474
|
+
if (wrapped_timeout || unwrapped_timeout || unwrapped_disconnect) && (timeout_retries < retry_timeout)
|
475
|
+
timeout_retries += 1
|
476
|
+
retry
|
477
|
+
else
|
478
|
+
timeout_retries = 0
|
479
|
+
raise e
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
yield(row_key, more_cols)
|
484
|
+
if more_cols.size < max_additional_column_count
|
485
|
+
last_column_name = nil
|
486
|
+
else
|
487
|
+
last_column_name = more_cols.last.column.name
|
488
|
+
end
|
413
489
|
end
|
414
490
|
end
|
415
491
|
end
|
492
|
+
|
493
|
+
break if rows.size < max_row_count
|
494
|
+
start_row = rows.keys.last
|
416
495
|
end
|
417
496
|
end
|
418
497
|
|
@@ -573,17 +652,8 @@ module RightSupport::DB
|
|
573
652
|
# === Return
|
574
653
|
# true:: Always return true
|
575
654
|
def reconnect
|
576
|
-
config = env_config
|
577
|
-
|
578
655
|
return false if keyspace.nil?
|
579
|
-
|
580
|
-
thrift_client_options = {:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT}
|
581
|
-
thrift_client_options.merge!({:protocol => Thrift::BinaryProtocolAccelerated})\
|
582
|
-
if defined? Thrift::BinaryProtocolAccelerated
|
583
|
-
|
584
|
-
connection = Cassandra.new(keyspace, config["server"], thrift_client_options)
|
585
|
-
connection.disable_node_auto_discovery!
|
586
|
-
@@connections[keyspace] = connection
|
656
|
+
@@connections[keyspace] = get_connection
|
587
657
|
true
|
588
658
|
end
|
589
659
|
|
@@ -20,17 +20,99 @@
|
|
20
20
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
|
23
|
+
require 'ipaddr'
|
23
24
|
require 'socket'
|
24
25
|
require 'uri'
|
25
26
|
|
26
27
|
module RightSupport::Net
|
28
|
+
|
29
|
+
# IPAddr does not support returning a block of addresses in CIDR notation.
|
30
|
+
class IPNet < ::IPAddr
|
31
|
+
def cidr_block
|
32
|
+
# Ruby Math:: does not have a log2() function :(
|
33
|
+
cidr_block = 32 - (Math.log(self.to_range.count) / Math.log(2)).to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
# Don't want to override to_s here since we also
|
37
|
+
# use this during URI transformation and need a way
|
38
|
+
# to get the raw dotted-quad address
|
39
|
+
#
|
40
|
+
def to_cidr
|
41
|
+
"#{self.to_s}/#{self.cidr_block}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# A ResolvedEndpoint represents the resolution of a host which can contain
|
46
|
+
# multiple IP addresses or entire IP address ranges of varying size.
|
47
|
+
class ResolvedEndpoint
|
48
|
+
attr_accessor :uri
|
49
|
+
|
50
|
+
def initialize(ips, opts={})
|
51
|
+
@ip_addresses = []
|
52
|
+
@uri = opts[:uri]
|
53
|
+
|
54
|
+
ips.to_a.each do |address|
|
55
|
+
@ip_addresses << IPNet.new(address)
|
56
|
+
end
|
57
|
+
|
58
|
+
unless self.all_hosts? || @uri == nil
|
59
|
+
raise URI::InvalidURIError, "Cannot resolve URI with CIDR block bigger than a single host" unless self.all_hosts?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def addresses()
|
64
|
+
@ip_addresses.map do |addr|
|
65
|
+
if addr.to_range.count == 1 && @uri != nil
|
66
|
+
transformed_uri = uri.dup
|
67
|
+
transformed_uri.host = addr.to_s
|
68
|
+
transformed_uri.to_s
|
69
|
+
else
|
70
|
+
addr
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias addrs :addresses
|
75
|
+
|
76
|
+
def blocks()
|
77
|
+
@ip_addresses.map {|addr| addr.to_cidr}
|
78
|
+
end
|
79
|
+
|
80
|
+
def all_hosts?()
|
81
|
+
@ip_addresses.all? {|addr| addr.to_range.count == 1}
|
82
|
+
end
|
83
|
+
|
84
|
+
def ==(another_endpoint)
|
85
|
+
another_endpoint.addresses.all? {|addr| addresses.member? addr } && another_endpoint.uri == @uri
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
27
89
|
module DNS
|
28
90
|
DEFAULT_RESOLVE_OPTIONS = {
|
29
91
|
:address_family => Socket::AF_INET,
|
30
92
|
:socket_type => Socket::SOCK_STREAM,
|
31
93
|
:protocol => Socket::IPPROTO_TCP,
|
32
|
-
:retry => 3
|
33
|
-
|
94
|
+
:retry => 3,
|
95
|
+
:uri => nil,
|
96
|
+
}.freeze
|
97
|
+
|
98
|
+
STATIC_HOSTNAME_TRANSLATIONS = {
|
99
|
+
# This list of CIDR blocks comes directly from Amazon and
|
100
|
+
# can be found here: https://forums.aws.amazon.com/ann.jspa?annID=2051
|
101
|
+
'cf-mirror.rightscale.com' => [ '54.192.0.0/16',
|
102
|
+
'54.230.0.0/16',
|
103
|
+
'54.239.128.0/18',
|
104
|
+
'54.240.128.0/18',
|
105
|
+
'204.246.164.0/22',
|
106
|
+
'204.246.168.0/22',
|
107
|
+
'204.246.174.0/23',
|
108
|
+
'204.246.176.0/20',
|
109
|
+
'205.251.192.0/19',
|
110
|
+
'205.251.249.0/24',
|
111
|
+
'205.251.250.0/23',
|
112
|
+
'205.251.252.0/23',
|
113
|
+
'205.251.254.0/24',
|
114
|
+
'216.137.32.0/19' ],
|
115
|
+
}.freeze
|
34
116
|
|
35
117
|
# Resolve a set of DNS hostnames to the individual IP addresses to which they map. Only handles
|
36
118
|
# IPv4 addresses.
|
@@ -81,10 +163,37 @@ module RightSupport::Net
|
|
81
163
|
# @raise URI::InvalidURIError if endpoints contains an invalid or URI
|
82
164
|
# @raise SocketError if endpoints contains an invalid or unresolvable hostname
|
83
165
|
def self.resolve(endpoints, opts={})
|
84
|
-
|
166
|
+
opts = DEFAULT_RESOLVE_OPTIONS.merge(opts)
|
167
|
+
endpoints = Array(endpoints)
|
168
|
+
|
169
|
+
retries = 0
|
85
170
|
resolved_endpoints = []
|
86
|
-
|
87
|
-
|
171
|
+
|
172
|
+
endpoints.each do |endpoint|
|
173
|
+
begin
|
174
|
+
resolved_endpoint = nil
|
175
|
+
if endpoint.include?(':')
|
176
|
+
# It contains a colon, therefore it must be a URI -- we don't support IPv6
|
177
|
+
uri = URI.parse(endpoint)
|
178
|
+
hostname = uri.host
|
179
|
+
raise URI::InvalidURIError, "Could not parse host component of URI" unless hostname
|
180
|
+
|
181
|
+
resolved_endpoint = resolve_endpoint(hostname, opts.merge(:uri=>uri))
|
182
|
+
else
|
183
|
+
resolved_endpoint = resolve_endpoint(endpoint, opts)
|
184
|
+
end
|
185
|
+
resolved_endpoints << resolved_endpoint
|
186
|
+
rescue SocketError => e
|
187
|
+
retries += 1
|
188
|
+
if retries < opts[:retry]
|
189
|
+
retry
|
190
|
+
else
|
191
|
+
raise
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
resolved_endpoints
|
88
197
|
end
|
89
198
|
|
90
199
|
# Similar to resolve, but return a hash of { hostnames => [endpoints] }
|
@@ -113,52 +222,44 @@ module RightSupport::Net
|
|
113
222
|
# @raise URI::InvalidURIError if endpoints contains an invalid or URI
|
114
223
|
# @raise SocketError if endpoints contains an invalid or unresolvable hostname
|
115
224
|
def self.resolve_with_hostnames(endpoints, opts={})
|
116
|
-
opts = DEFAULT_RESOLVE_OPTIONS.merge(opts)
|
117
|
-
endpoints = [endpoints] unless endpoints.respond_to?(:each)
|
118
|
-
|
119
225
|
hostname_hash = {}
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
begin
|
124
|
-
resolved_endpoints = []
|
125
|
-
if endpoint.include?(':')
|
126
|
-
# It contains a colon, therefore it must be a URI -- we don't support IPv6
|
127
|
-
uri = URI.parse(endpoint)
|
128
|
-
hostname = uri.host
|
129
|
-
raise URI::InvalidURIError, "Could not parse host component of URI" unless hostname
|
226
|
+
endpoints.each {|endpoint| hostname_hash[endpoint] = resolve(endpoint, opts).first}
|
227
|
+
hostname_hash
|
228
|
+
end
|
130
229
|
|
131
|
-
|
132
|
-
opts[:address_family], opts[:socket_type], opts[:protocol])
|
230
|
+
private
|
133
231
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
232
|
+
# Lookup the address(es) associated with the given endpoints.
|
233
|
+
#
|
234
|
+
# Perform an address lookup in this order:
|
235
|
+
# 1. Hardcoded translation table.
|
236
|
+
# 2. System address lookup (e.g. DNS, hosts files...etc)
|
237
|
+
#
|
238
|
+
# Although this method does accept IPv4 dotted-quad addresses as input, it does not accept
|
239
|
+
# IPv6 addresses. However, given hostnames as input, one _can_ resolve the hostnames
|
240
|
+
# to IPv6 addresses by specifying the appropriate address_family in the options.
|
241
|
+
#
|
242
|
+
# It should never be necessary to specify a different :socket_type or :protocol, but these
|
243
|
+
# options are exposed just in case.
|
244
|
+
#
|
245
|
+
# @param [String] endpoint as a hostname or IPv4 address
|
246
|
+
# @option opts [Integer] :retry number of times to retry SocketError; default is 3
|
247
|
+
# @option opts [Integer] :address_family what kind of IP addresses to resolve; default is Socket::AF_INET (IPv4)
|
248
|
+
# @option opts [Integer] :socket_type socket-type context to pass to getaddrinfo, default is Socket::SOCK_STREAM
|
249
|
+
# @option opts [Integer] :protocol protocol context to pass to getaddrinfo, default is Socket::IPPROTO_TCP
|
250
|
+
#
|
251
|
+
# @return [Array<String>] List of resolved IPv4/IPv6 addresses.
|
252
|
+
#
|
253
|
+
# @raise SocketError if endpoints contains an invalid or unresolvable hostname
|
254
|
+
def self.resolve_endpoint(endpoint, opts=DEFAULT_RESOLVE_OPTIONS)
|
255
|
+
if STATIC_HOSTNAME_TRANSLATIONS.has_key?(endpoint)
|
256
|
+
ResolvedEndpoint.new(STATIC_HOSTNAME_TRANSLATIONS[endpoint], opts)
|
257
|
+
else
|
258
|
+
infos = Socket.getaddrinfo(endpoint, nil,
|
259
|
+
opts[:address_family], opts[:socket_type], opts[:protocol])
|
260
|
+
ResolvedEndpoint.new(infos.map { |info| info[3] }, opts)
|
157
261
|
end
|
158
|
-
|
159
|
-
hostname_hash
|
160
262
|
end
|
161
|
-
|
162
263
|
end
|
163
264
|
end
|
164
265
|
|
@@ -204,7 +204,7 @@ module RightSupport::Net
|
|
204
204
|
# === Return
|
205
205
|
# Return the first hostname that resolved to the IP (there should only ever be one)
|
206
206
|
def lookup_hostname(endpoint)
|
207
|
-
@resolved_hostnames.select{ |k,v| v.include?(endpoint) }.shift[0]
|
207
|
+
@resolved_hostnames.select{ |k,v| v.addresses.include?(endpoint) }.shift[0]
|
208
208
|
end
|
209
209
|
|
210
210
|
# Perform a request.
|
@@ -369,7 +369,7 @@ module RightSupport::Net
|
|
369
369
|
def resolve
|
370
370
|
@resolved_hostnames = RightSupport::Net::DNS.resolve_with_hostnames(@endpoints)
|
371
371
|
resolved_endpoints = []
|
372
|
-
@resolved_hostnames.each_value{ |v| resolved_endpoints.concat(v) }
|
372
|
+
@resolved_hostnames.each_value{ |v| resolved_endpoints.concat(v.addrs.map {|addr| addr.to_s}) }
|
373
373
|
logger.info("RequestBalancer: resolved #{@endpoints.inspect} to #{resolved_endpoints.inspect}") if resolved_endpoints != @ips
|
374
374
|
@ips = resolved_endpoints
|
375
375
|
@policy.set_endpoints(@ips)
|
data/right_support.gemspec
CHANGED
@@ -4,14 +4,14 @@
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name =
|
8
|
-
s.version = "2.8.
|
7
|
+
s.name = "right_support"
|
8
|
+
s.version = "2.8.6"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Tony Spataro", "Sergey Sergyenko", "Ryan Williamson", "Lee Kirchhoff", "Alexey Karpik", "Scott Messier"]
|
12
|
-
s.date =
|
13
|
-
s.description =
|
14
|
-
s.email =
|
12
|
+
s.date = "2013-12-18"
|
13
|
+
s.description = "A toolkit of useful, reusable foundation code created by RightScale."
|
14
|
+
s.email = "support@rightscale.com"
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
17
|
"README.rdoc"
|
@@ -136,14 +136,13 @@ Gem::Specification.new do |s|
|
|
136
136
|
"spec/validation/openssl_spec.rb",
|
137
137
|
"spec/validation/ssh_spec.rb"
|
138
138
|
]
|
139
|
-
s.homepage =
|
139
|
+
s.homepage = "https://github.com/rightscale/right_support"
|
140
140
|
s.licenses = ["MIT"]
|
141
141
|
s.require_paths = ["lib"]
|
142
|
-
s.rubygems_version =
|
143
|
-
s.summary =
|
142
|
+
s.rubygems_version = "1.8.15"
|
143
|
+
s.summary = "Reusable foundation code."
|
144
144
|
|
145
145
|
if s.respond_to? :specification_version then
|
146
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
147
146
|
s.specification_version = 3
|
148
147
|
|
149
148
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
@@ -58,14 +58,14 @@ describe RightSupport::DB::CassandraModel do
|
|
58
58
|
,'ring1, ring2, ring3' => ['ring1', 'ring2', 'ring3'] \
|
59
59
|
,['ring1', 'ring2', 'ring3'] => ['ring1', 'ring2', 'ring3']
|
60
60
|
}.each do |config_string, congig_test|
|
61
|
-
it "
|
61
|
+
it "should successfully intialize from #{config_string.inspect}" do
|
62
62
|
|
63
63
|
|
64
64
|
old_rack_env = ENV['RACK_ENV']
|
65
65
|
begin
|
66
66
|
ENV['RACK_ENV'] = env
|
67
67
|
|
68
|
-
flexmock(Cassandra).should_receive(:new).with(/#{default_keyspace}_#{env}/, congig_test , {:timeout=>10}).and_return(default_keyspace_connection)
|
68
|
+
flexmock(Cassandra).should_receive(:new).with(/#{default_keyspace}_#{env}/, congig_test , {:timeout=>10, :server_retry_period=>nil}).and_return(default_keyspace_connection)
|
69
69
|
default_keyspace_connection.should_receive(:disable_node_auto_discovery!).and_return(true)
|
70
70
|
|
71
71
|
|
@@ -81,4 +81,4 @@ describe RightSupport::DB::CassandraModel do
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
end
|
84
|
-
end
|
84
|
+
end
|
data/spec/net/dns_spec.rb
CHANGED
@@ -22,7 +22,7 @@ describe RightSupport::Net::DNS do
|
|
22
22
|
context :resolve do
|
23
23
|
context 'given default :retry => 3' do
|
24
24
|
let(:endpoint) { 'www.example.com' }
|
25
|
-
let(:output) { ['1.1.1.1', '2.2.2.2'] }
|
25
|
+
let(:output) { [RightSupport::Net::ResolvedEndpoint.new(['1.1.1.1', '2.2.2.2'])] }
|
26
26
|
|
27
27
|
it 'retries SocketError' do
|
28
28
|
mock_getaddrinfo('www.example.com', SocketError)
|
@@ -55,7 +55,7 @@ describe RightSupport::Net::DNS do
|
|
55
55
|
context 'given various endpoint formats' do
|
56
56
|
context 'e.g. a DNS hostname' do
|
57
57
|
let(:endpoint) { 'www.example.com' }
|
58
|
-
let(:output) { ['1.1.1.1', '2.2.2.2'] }
|
58
|
+
let(:output) { [RightSupport::Net::ResolvedEndpoint.new(['1.1.1.1', '2.2.2.2'])] }
|
59
59
|
|
60
60
|
it 'resolves to IP addresses' do
|
61
61
|
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
@@ -65,7 +65,7 @@ describe RightSupport::Net::DNS do
|
|
65
65
|
|
66
66
|
context 'e.g. an IPv4 address' do
|
67
67
|
let(:endpoint) { '127.0.0.1' }
|
68
|
-
let(:output) { ['127.0.0.1'] }
|
68
|
+
let(:output) { [RightSupport::Net::ResolvedEndpoint.new(['127.0.0.1'])] }
|
69
69
|
|
70
70
|
it 'resolves to the same address' do
|
71
71
|
mock_getaddrinfo('127.0.0.1', ['127.0.0.1'])
|
@@ -79,7 +79,7 @@ describe RightSupport::Net::DNS do
|
|
79
79
|
|
80
80
|
it 'resolves to URLs with addresses substituted' do
|
81
81
|
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
82
|
-
subject.resolve(endpoint).should == output
|
82
|
+
subject.resolve(endpoint).first.addrs.should == output
|
83
83
|
end
|
84
84
|
|
85
85
|
context 'with a path component' do
|
@@ -88,7 +88,7 @@ describe RightSupport::Net::DNS do
|
|
88
88
|
|
89
89
|
it 'resolves to URLs with path component preserved' do
|
90
90
|
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
91
|
-
subject.resolve(endpoint).should == output
|
91
|
+
subject.resolve(endpoint).first.addrs.should == output
|
92
92
|
end
|
93
93
|
end
|
94
94
|
end
|
@@ -111,13 +111,13 @@ describe RightSupport::Net::DNS do
|
|
111
111
|
|
112
112
|
context 'e.g. several hostnames' do
|
113
113
|
let(:endpoints) { ['www.example.com', 'www.example.net'] }
|
114
|
-
let(:output) { ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4'] }
|
114
|
+
let(:output) { [RightSupport::Net::ResolvedEndpoint.new(['1.1.1.1', '2.2.2.2']), RightSupport::Net::ResolvedEndpoint.new(['3.3.3.3', '4.4.4.4'])] }
|
115
115
|
|
116
116
|
it 'resolves to IP addresses' do
|
117
117
|
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
118
118
|
mock_getaddrinfo('www.example.net', ['3.3.3.3', '4.4.4.4'])
|
119
119
|
|
120
|
-
subject.resolve(endpoints).
|
120
|
+
subject.resolve(endpoints).should == output
|
121
121
|
end
|
122
122
|
end
|
123
123
|
|
@@ -129,7 +129,42 @@ describe RightSupport::Net::DNS do
|
|
129
129
|
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
130
130
|
mock_getaddrinfo('www.example.net', ['3.3.3.3', '4.4.4.4'])
|
131
131
|
|
132
|
-
subject.resolve(endpoints).
|
132
|
+
subject.resolve(endpoints).map {|ep| ep.addrs}.flatten == output
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'requesting CIDR blocks' do
|
138
|
+
context 'DNS resolvable addresses' do
|
139
|
+
let(:endpoint) { 'www.example.com' }
|
140
|
+
let(:output) { ['1.1.1.1/32', '2.2.2.2/32'] }
|
141
|
+
|
142
|
+
it 'resolves hostname to CIDR /32 blocks' do
|
143
|
+
mock_getaddrinfo('www.example.com', ['1.1.1.1', '2.2.2.2'])
|
144
|
+
subject.resolve(endpoint).first.blocks.should == output
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'resolves IP addresses to CIDR /32 blocks' do
|
148
|
+
mock_getaddrinfo('1.1.1.1', ['1.1.1.1'])
|
149
|
+
mock_getaddrinfo('2.2.2.2', ['2.2.2.2'])
|
150
|
+
subject.resolve(['1.1.1.1', '2.2.2.2']).map {|endpoint| endpoint.blocks}.flatten.should =~ output
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'refuses to resolve a URI having a CIDR block < /32' do
|
154
|
+
lambda do
|
155
|
+
subject.resolve("http://cf-mirror.rightscale.com")
|
156
|
+
end.should raise_error(URI::InvalidURIError)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'Static CIDR blocks' do
|
161
|
+
let(:endpoint) { 'cf-mirror.rightscale.com' }
|
162
|
+
|
163
|
+
it 'resolves hostname to static CIDR blocks' do
|
164
|
+
subject.resolve(endpoint).first.blocks.each do |addr|
|
165
|
+
# Addresses should all be in CIDR form and not single /32 addresses
|
166
|
+
addr.should =~ /^\d+(?:\.\d+){3}\/(?:#{1.upto(31).to_a.join('|')})$/
|
167
|
+
end
|
133
168
|
end
|
134
169
|
end
|
135
170
|
end
|
@@ -138,8 +173,8 @@ describe RightSupport::Net::DNS do
|
|
138
173
|
context :resolve_with_hostnames do
|
139
174
|
context 'given common inputs' do
|
140
175
|
let(:endpoints) { ['www.example1.com','www.example2.com'] }
|
141
|
-
let(:output) { {'www.example1.com'=>['1.1.1.1', '2.2.2.2'],
|
142
|
-
'www.example2.com'=>['3.3.3.3', '4.4.4.4']} }
|
176
|
+
let(:output) { {'www.example1.com'=>RightSupport::Net::ResolvedEndpoint.new(['1.1.1.1', '2.2.2.2']),
|
177
|
+
'www.example2.com'=>RightSupport::Net::ResolvedEndpoint.new(['3.3.3.3', '4.4.4.4'])} }
|
143
178
|
|
144
179
|
it 'resolves to IP addresses' do
|
145
180
|
mock_getaddrinfo('www.example1.com', ['1.1.1.1', '2.2.2.2'])
|
@@ -91,6 +91,10 @@ describe RightSupport::Net::RequestBalancer do
|
|
91
91
|
@health_checks.should == expect * (yellow_states - 1)
|
92
92
|
end
|
93
93
|
|
94
|
+
def make_endpoint(addresses)
|
95
|
+
RightSupport::Net::ResolvedEndpoint.new(addresses)
|
96
|
+
end
|
97
|
+
|
94
98
|
context :initialize do
|
95
99
|
it 'requires a list of endpoint URLs' do
|
96
100
|
lambda do
|
@@ -267,7 +271,7 @@ describe RightSupport::Net::RequestBalancer do
|
|
267
271
|
context 'with :resolve option' do
|
268
272
|
before(:each) do
|
269
273
|
flexmock(RightSupport::Net::DNS).should_receive(:resolve_with_hostnames).
|
270
|
-
with(['host1', 'host2']).and_return({'host1' => ['1.1.1.1', '2.2.2.2'], 'host2' => ['3.3.3.3']})
|
274
|
+
with(['host1', 'host2']).and_return({'host1' => make_endpoint(['1.1.1.1', '2.2.2.2']), 'host2' => make_endpoint(['3.3.3.3'])})
|
271
275
|
@balancer = RightSupport::Net::RequestBalancer.new(['host1', 'host2'], :resolve => 15)
|
272
276
|
end
|
273
277
|
|
@@ -482,10 +486,10 @@ describe RightSupport::Net::RequestBalancer do
|
|
482
486
|
context 'with :resolve option' do
|
483
487
|
before(:each) do
|
484
488
|
@endpoints = ['host1', 'host2', 'host3', 'host4']
|
485
|
-
@resolved_set_1 = {'host1' => ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4']}
|
486
|
-
@resolved_set_2 = {'host1'=>['5.5.5.5'],'host2'=>['6.6.6.6'],'host3'=>['7.7.7.7'],'host4'=>['8.8.8.8']}
|
489
|
+
@resolved_set_1 = {'host1' => make_endpoint(['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4'])}
|
490
|
+
@resolved_set_2 = {'host1'=> make_endpoint(['5.5.5.5']),'host2'=>make_endpoint(['6.6.6.6']),'host3'=>make_endpoint(['7.7.7.7']),'host4'=>make_endpoint(['8.8.8.8'])}
|
487
491
|
@resolved_set_2_array = []
|
488
|
-
@resolved_set_2.each_value{ |v| @resolved_set_2_array.concat(v) }
|
492
|
+
@resolved_set_2.each_value{ |v| @resolved_set_2_array.concat(v.addrs) }
|
489
493
|
@dns = flexmock(RightSupport::Net::DNS)
|
490
494
|
end
|
491
495
|
|
@@ -495,7 +499,7 @@ describe RightSupport::Net::RequestBalancer do
|
|
495
499
|
|
496
500
|
@rb.request { true }
|
497
501
|
@policy = @rb.instance_variable_get("@policy")
|
498
|
-
@resolved_set_1['host1'].include?(@policy.next.first).should be_true
|
502
|
+
@resolved_set_1['host1'].addrs.include?(@policy.next.first).should be_true
|
499
503
|
end
|
500
504
|
|
501
505
|
it 're-resolves list of ip addresses if TTL is expired' do
|
@@ -504,7 +508,7 @@ describe RightSupport::Net::RequestBalancer do
|
|
504
508
|
|
505
509
|
@rb.request { true }
|
506
510
|
@policy = @rb.instance_variable_get("@policy")
|
507
|
-
@resolved_set_1['host1'].include?(@policy.next.first).should be_true
|
511
|
+
@resolved_set_1['host1'].addrs.include?(@policy.next.first).should be_true
|
508
512
|
|
509
513
|
@rb.instance_variable_set("@resolved_at", Time.now.to_i - 16)
|
510
514
|
@rb.request { true }
|
data/spec/stats/helpers_spec.rb
CHANGED
@@ -431,7 +431,7 @@ describe RightSupport::Stats do
|
|
431
431
|
"/opt/rightscale/right_link/common/lib/common/serializer.rb:133:in `cascade_serializers'"}]}}
|
432
432
|
RightSupport::Stats.exceptions_str(exceptions, "").should ==
|
433
433
|
"receive total: 2, most recent:\n" +
|
434
|
-
"(2)
|
434
|
+
"(2) #{Time.at(exceptions['receive']['recent'].first['when']).strftime("%a %b %d %H:%M:%S")} RightScale::Serializer::SerializationError: Could not \n" +
|
435
435
|
" load packet using [RightScale::SecureSerializer] (Failed to load with \n" +
|
436
436
|
" RightScale::SecureSerializer (TypeError: can't convert nil into String in /opt/\n" +
|
437
437
|
" rightscale/right_link/common/lib/common/security/signature.rb:56:in \n" +
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: right_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 35
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 8
|
9
|
-
-
|
10
|
-
version: 2.8.
|
9
|
+
- 6
|
10
|
+
version: 2.8.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tony Spataro
|
@@ -20,8 +20,7 @@ autorequire:
|
|
20
20
|
bindir: bin
|
21
21
|
cert_chain: []
|
22
22
|
|
23
|
-
date: 2013-
|
24
|
-
default_executable:
|
23
|
+
date: 2013-12-19 00:00:00 Z
|
25
24
|
dependencies:
|
26
25
|
- !ruby/object:Gem::Dependency
|
27
26
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -34,10 +33,10 @@ dependencies:
|
|
34
33
|
- 0
|
35
34
|
- 9
|
36
35
|
version: "0.9"
|
37
|
-
requirement: *id001
|
38
36
|
type: :development
|
39
|
-
|
37
|
+
requirement: *id001
|
40
38
|
prerelease: false
|
39
|
+
name: rake
|
41
40
|
- !ruby/object:Gem::Dependency
|
42
41
|
version_requirements: &id002 !ruby/object:Gem::Requirement
|
43
42
|
none: false
|
@@ -50,10 +49,10 @@ dependencies:
|
|
50
49
|
- 8
|
51
50
|
- 3
|
52
51
|
version: 1.8.3
|
53
|
-
requirement: *id002
|
54
52
|
type: :development
|
55
|
-
|
53
|
+
requirement: *id002
|
56
54
|
prerelease: false
|
55
|
+
name: jeweler
|
57
56
|
- !ruby/object:Gem::Dependency
|
58
57
|
version_requirements: &id003 !ruby/object:Gem::Requirement
|
59
58
|
none: false
|
@@ -65,10 +64,10 @@ dependencies:
|
|
65
64
|
- 1
|
66
65
|
- 0
|
67
66
|
version: "1.0"
|
68
|
-
requirement: *id003
|
69
67
|
type: :development
|
70
|
-
|
68
|
+
requirement: *id003
|
71
69
|
prerelease: false
|
70
|
+
name: right_develop
|
72
71
|
- !ruby/object:Gem::Dependency
|
73
72
|
version_requirements: &id004 !ruby/object:Gem::Requirement
|
74
73
|
none: false
|
@@ -80,10 +79,10 @@ dependencies:
|
|
80
79
|
- 0
|
81
80
|
- 10
|
82
81
|
version: "0.10"
|
83
|
-
requirement: *id004
|
84
82
|
type: :development
|
85
|
-
|
83
|
+
requirement: *id004
|
86
84
|
prerelease: false
|
85
|
+
name: ruby-debug
|
87
86
|
- !ruby/object:Gem::Dependency
|
88
87
|
version_requirements: &id005 !ruby/object:Gem::Requirement
|
89
88
|
none: false
|
@@ -96,10 +95,10 @@ dependencies:
|
|
96
95
|
- 11
|
97
96
|
- 6
|
98
97
|
version: 0.11.6
|
99
|
-
requirement: *id005
|
100
98
|
type: :development
|
101
|
-
|
99
|
+
requirement: *id005
|
102
100
|
prerelease: false
|
101
|
+
name: ruby-debug19
|
103
102
|
- !ruby/object:Gem::Dependency
|
104
103
|
version_requirements: &id006 !ruby/object:Gem::Requirement
|
105
104
|
none: false
|
@@ -112,10 +111,10 @@ dependencies:
|
|
112
111
|
- 4
|
113
112
|
- 2
|
114
113
|
version: 2.4.2
|
115
|
-
requirement: *id006
|
116
114
|
type: :development
|
117
|
-
|
115
|
+
requirement: *id006
|
118
116
|
prerelease: false
|
117
|
+
name: rdoc
|
119
118
|
- !ruby/object:Gem::Dependency
|
120
119
|
version_requirements: &id007 !ruby/object:Gem::Requirement
|
121
120
|
none: false
|
@@ -127,10 +126,10 @@ dependencies:
|
|
127
126
|
- 1
|
128
127
|
- 0
|
129
128
|
version: "1.0"
|
130
|
-
requirement: *id007
|
131
129
|
type: :development
|
132
|
-
|
130
|
+
requirement: *id007
|
133
131
|
prerelease: false
|
132
|
+
name: flexmock
|
134
133
|
- !ruby/object:Gem::Dependency
|
135
134
|
version_requirements: &id008 !ruby/object:Gem::Requirement
|
136
135
|
none: false
|
@@ -143,10 +142,10 @@ dependencies:
|
|
143
142
|
- 0
|
144
143
|
- 0
|
145
144
|
version: 1.0.0
|
146
|
-
requirement: *id008
|
147
145
|
type: :development
|
148
|
-
|
146
|
+
requirement: *id008
|
149
147
|
prerelease: false
|
148
|
+
name: syntax
|
150
149
|
- !ruby/object:Gem::Dependency
|
151
150
|
version_requirements: &id009 !ruby/object:Gem::Requirement
|
152
151
|
none: false
|
@@ -158,10 +157,10 @@ dependencies:
|
|
158
157
|
- 1
|
159
158
|
- 5
|
160
159
|
version: "1.5"
|
161
|
-
requirement: *id009
|
162
160
|
type: :development
|
163
|
-
|
161
|
+
requirement: *id009
|
164
162
|
prerelease: false
|
163
|
+
name: nokogiri
|
165
164
|
description: A toolkit of useful, reusable foundation code created by RightScale.
|
166
165
|
email: support@rightscale.com
|
167
166
|
executables: []
|
@@ -290,7 +289,6 @@ files:
|
|
290
289
|
- spec/stats/helpers_spec.rb
|
291
290
|
- spec/validation/openssl_spec.rb
|
292
291
|
- spec/validation/ssh_spec.rb
|
293
|
-
has_rdoc: true
|
294
292
|
homepage: https://github.com/rightscale/right_support
|
295
293
|
licenses:
|
296
294
|
- MIT
|
@@ -320,7 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
320
318
|
requirements: []
|
321
319
|
|
322
320
|
rubyforge_project:
|
323
|
-
rubygems_version: 1.
|
321
|
+
rubygems_version: 1.8.15
|
324
322
|
signing_key:
|
325
323
|
specification_version: 3
|
326
324
|
summary: Reusable foundation code.
|