right_support 2.8.3 → 2.8.6
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|