scriptroute 0.4.14
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.
- checksums.yaml +7 -0
- data/bin/sr-ally +21 -0
- data/bin/sr-liveness +11 -0
- data/bin/sr-ping-T +69 -0
- data/bin/sr-rockettrace +51 -0
- data/bin/sr-traceroute +35 -0
- data/bin/tulip +183 -0
- data/lib/scriptroute.rb +327 -0
- data/lib/scriptroute/ally.rb +449 -0
- data/lib/scriptroute/commando.rb +228 -0
- data/lib/scriptroute/fixclock +1260 -0
- data/lib/scriptroute/liveness.rb +129 -0
- data/lib/scriptroute/nameify.rb +127 -0
- data/lib/scriptroute/packets.rb +800 -0
- data/lib/scriptroute/rockettrace.rb +181 -0
- data/lib/scriptroute/tulip/helper.rb +730 -0
- data/lib/scriptroute/tulip/loss.rb +145 -0
- data/lib/scriptroute/tulip/queuing.rb +248 -0
- data/lib/scriptroute/tulip/reordering.rb +129 -0
- data/test/test_bins.rb +20 -0
- data/test/test_scriptroute.rb +155 -0
- metadata +71 -0
@@ -0,0 +1,449 @@
|
|
1
|
+
|
2
|
+
# Ally is a class;
|
3
|
+
# the class object caches the results of previous tests.
|
4
|
+
# an object of the class is the name / object space in which
|
5
|
+
# a test is run.
|
6
|
+
|
7
|
+
$have_undns = begin
|
8
|
+
require 'undns'
|
9
|
+
true
|
10
|
+
rescue LoadError, SecurityError
|
11
|
+
false
|
12
|
+
end
|
13
|
+
$have_socket = begin
|
14
|
+
require 'socket'
|
15
|
+
true
|
16
|
+
rescue LoadError, SecurityError
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
module Scriptroute
|
21
|
+
class Ally
|
22
|
+
|
23
|
+
# the cache of previously seen results is a class variable.
|
24
|
+
@@result_cache = Hash.new;
|
25
|
+
|
26
|
+
# use bdb41 to save the cache of results persistently, in case we
|
27
|
+
# would like to restore it. This sets the cache to be on disk
|
28
|
+
# (so we can read from it, and it will exist after the script)
|
29
|
+
# instead of in memory (created and destroyed with every invocation).
|
30
|
+
# @param [String] dbfilename where to store the cache.
|
31
|
+
# @return [void]
|
32
|
+
# @note the cache does not expire entries. The cache should probably not
|
33
|
+
# be used after, say, a month, since topologies can change.
|
34
|
+
# @note if bdb41 cannot be loaded, the cache will not be made persistent, and
|
35
|
+
# an error will print to stderr.
|
36
|
+
def Ally.make_result_cache_persistent(dbfilename)
|
37
|
+
begin
|
38
|
+
require "bdb41"
|
39
|
+
@@result_cache = BDB::Hash.new(dbfilename, nil, BDB::CREATE)
|
40
|
+
rescue LoadError
|
41
|
+
$stderr.puts "Unable to make result cache persistent: install bdb41 (libdb4.1-ruby)"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# Iterate through the result cache, yielding each pair of IP addresses
|
45
|
+
# found to be aliases.
|
46
|
+
# @yield [String,String]
|
47
|
+
def Ally.each_alias
|
48
|
+
@@result_cache.each { |k,v|
|
49
|
+
if v =~ /^ALIAS/ then
|
50
|
+
yield k.split(":")
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# keys in the cache are concatenations of IP addresses.
|
56
|
+
# since alias relations are symmetric (reflexive?), the
|
57
|
+
# addresses are sorted first.
|
58
|
+
# @return [String] a key for use in the cache hashtable.
|
59
|
+
def Ally.to_key(ip_a, ip_b)
|
60
|
+
[ ip_a, ip_b ].sort.join(':')
|
61
|
+
end
|
62
|
+
|
63
|
+
# a quick shorthand for finding where our object belongs
|
64
|
+
# in the cache.
|
65
|
+
# @return [String] a key for use in the cache hashtable.
|
66
|
+
def my_key
|
67
|
+
Ally.to_key(@a.ip_dst, @b.ip_dst)
|
68
|
+
end
|
69
|
+
|
70
|
+
# when we've decided that something is an alias, not an alias
|
71
|
+
# or unknown, we call these functions. they may be overridden
|
72
|
+
# by a derived class to create different (perhaps hooked) behavior.
|
73
|
+
# however, super() should be called first to cache the result.
|
74
|
+
|
75
|
+
# @param msg [String] why we cannot figure out this pair
|
76
|
+
# @return [void]
|
77
|
+
def unknown(msg)
|
78
|
+
@verdict = "UNKNOWN. #{msg}"
|
79
|
+
@@result_cache[my_key] = @verdict;
|
80
|
+
throw :resolved # much like a return in the calling scope.
|
81
|
+
end
|
82
|
+
# @param msg [String] why we believe this pair to be aliases
|
83
|
+
# @return [void]
|
84
|
+
def is_alias(msg)
|
85
|
+
@verdict = "ALIAS! #{msg}"
|
86
|
+
@@result_cache[my_key] = @verdict;
|
87
|
+
throw :resolved
|
88
|
+
end
|
89
|
+
# @param msg [String] why we believe this pair to be not aliases
|
90
|
+
# @return [void]
|
91
|
+
def not_alias(msg)
|
92
|
+
@verdict = "NOT ALIAS. #{msg}"
|
93
|
+
@@result_cache[my_key] = @verdict;
|
94
|
+
throw :resolved
|
95
|
+
end
|
96
|
+
|
97
|
+
# a helper function to handle comparing ipid's, as they are
|
98
|
+
# unsigned short counters that can wrap.
|
99
|
+
# @return [Boolean]
|
100
|
+
def Ally.before(seq1,seq2)
|
101
|
+
diff = seq1-seq2
|
102
|
+
# emulate signed short arithmetic.
|
103
|
+
if (diff > 32767) then
|
104
|
+
diff-=65535;
|
105
|
+
elsif (diff < -32768) then
|
106
|
+
diff+=65535;
|
107
|
+
end
|
108
|
+
# puts "#{seq1} #{diff < 0 ? "" : "not"} before #{seq2}\n"
|
109
|
+
(diff < 0)
|
110
|
+
end
|
111
|
+
# a helper function to handle comparing ipid's, as they are
|
112
|
+
# unsigned short counters that can wrap.
|
113
|
+
# @return [Boolean]
|
114
|
+
def before
|
115
|
+
Ally.before(seq1,seq2)
|
116
|
+
end
|
117
|
+
|
118
|
+
# do a reverse lookup on an IP address. Be prepared to handle an exception,
|
119
|
+
# as in safe mode, this is not permitted.
|
120
|
+
# @param [String] ip the IP address to convert.
|
121
|
+
# @return [String] the output of Socket.gethostbyname
|
122
|
+
# @raise [RuntimeError] if gethostbyname throws an exception.
|
123
|
+
def ip_to_name(ip)
|
124
|
+
begin
|
125
|
+
(Socket.gethostbyname(ip)[0]).gsub(/\"/,'')
|
126
|
+
rescue => e
|
127
|
+
# provide a more informative exception.
|
128
|
+
raise "#{ip} #{e}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# if we can't tell using packets, try using undns to guess
|
133
|
+
# using the names attached to these interfaces.
|
134
|
+
def try_undns(msg)
|
135
|
+
# we can't tell using packets whether the two are aliases.
|
136
|
+
|
137
|
+
if(!$have_undns) then
|
138
|
+
# throws out (return implied)
|
139
|
+
unknown "#{msg}; undns not loaded."
|
140
|
+
end
|
141
|
+
|
142
|
+
# try a reverse lookup, then match the names using established
|
143
|
+
# rules. This requires access to the file system so won't
|
144
|
+
# work if run remotely. (it will jump to the rescue line and
|
145
|
+
# print unknown).
|
146
|
+
begin
|
147
|
+
# the next fragment is designed to try both lookups first,
|
148
|
+
# then try both through undns second. this means we won't
|
149
|
+
# complain about undns unless we would have had a chance of
|
150
|
+
# using it.
|
151
|
+
begin
|
152
|
+
a_name, b_name = [ @a.ip_dst, @b.ip_dst ].map { |dst|
|
153
|
+
ip_to_name(dst)
|
154
|
+
}
|
155
|
+
begin
|
156
|
+
a_uniq, b_uniq = [ a_name, b_name ].map { |name|
|
157
|
+
uniq = Undns.get_identifier(0, name)
|
158
|
+
if( uniq == nil || uniq == '') then
|
159
|
+
raise "#{name} lacks unique fragments"
|
160
|
+
end
|
161
|
+
uniq
|
162
|
+
}
|
163
|
+
if(a_uniq == b_uniq) then
|
164
|
+
is_alias "name: #{a_uniq}, otherwise #{msg}"
|
165
|
+
else
|
166
|
+
not_alias "name: #{a_uniq} != #{b_uniq} and #{msg}"
|
167
|
+
end
|
168
|
+
rescue => e
|
169
|
+
# rule them out if the names say different cities, even
|
170
|
+
# if we can't tell the specific pattern to prove that
|
171
|
+
# addresses are aliases. this is just an optimization --
|
172
|
+
# unknown is usually treated as "no" anyway.
|
173
|
+
a_city, b_city = [ a_name, b_name ].map { |name|
|
174
|
+
city = Undns.get_loc(0, name)
|
175
|
+
if( city == nil || city == '') then
|
176
|
+
raise "#{name} lacks unique fragments and city location"
|
177
|
+
end
|
178
|
+
city
|
179
|
+
}
|
180
|
+
if(a_city == b_city) then
|
181
|
+
unknown "#{msg}; undns failed and cities are the same: #{e}"
|
182
|
+
else
|
183
|
+
not_alias "name: cities #{a_city} != #{b_city} and #{msg}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
rescue SecurityError => e
|
188
|
+
unknown "#{msg}; undns failed (securityerror): #{e}"
|
189
|
+
rescue LoadError => e
|
190
|
+
unknown "#{msg}; loaderror undns failed: #{e}"
|
191
|
+
rescue => e
|
192
|
+
# unable to lookup, don't have undns, etc.
|
193
|
+
unknown "#{msg}; undns failed: #{e}"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# responses we can get include address unreachable, which means we're
|
198
|
+
# not talking to the intended router. Check first.
|
199
|
+
def assert_response_non_bogus(resp)
|
200
|
+
if( ! resp.response ) then
|
201
|
+
raise "(bug!) assert_response_non_bogus shouldn't check that a response was received"
|
202
|
+
end
|
203
|
+
if(@a.is_a?(Scriptroute::UDP)) then
|
204
|
+
# if we tried to probe using udp traceroute-like, we're expecting a port unreach.
|
205
|
+
# if we don't get it, likely filtered, try undns
|
206
|
+
if((! resp.response.packet.is_a?(Scriptroute::ICMP)) or
|
207
|
+
( resp.response.packet.icmp_type != Scriptroute::ICMP::ICMP_UNREACH ) or
|
208
|
+
( resp.response.packet.icmp_code != Scriptroute::ICMP::ICMP_UNREACH_PORT )) then
|
209
|
+
try_undns "filtered: #{resp.probe.packet.ip_dst}"
|
210
|
+
end
|
211
|
+
elsif(@a.is_a?(Scriptroute::TCP)) then
|
212
|
+
# if we tried to probe using tcp 80, we're expecting a tcp rst.
|
213
|
+
# if we don't get it, likely filtered, try undns
|
214
|
+
if(! resp.response.packet.is_a?(Scriptroute::TCP)) then
|
215
|
+
try_undns "filtered: #{resp.probe.packet.ip_dst}"
|
216
|
+
end
|
217
|
+
elsif(@a.is_a?(Scriptroute::ICMP) &&
|
218
|
+
@a.icmp_type == Scriptroute::ICMP_ECHO ) then
|
219
|
+
# if we tried to probe using tcp 80, we're expecting a tcp rst.
|
220
|
+
# if we don't get it, likely filtered, try undns
|
221
|
+
if((! resp.response.packet.is_a?(Scriptroute::ICMP)) or
|
222
|
+
resp.response.packet.icmp_type != Scriptroute::ICMP_ECHOREPLY) then
|
223
|
+
try_undns "filtered: #{resp.probe.packet.ip_dst}"
|
224
|
+
end
|
225
|
+
else
|
226
|
+
# we don't seem to know what to do.
|
227
|
+
fail "Can't recognize expected response to #{@a.to_s}."
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
def merc(exchange)
|
233
|
+
exchange.probe.packet.ip_dst + '->' + exchange.response.packet.ip_src
|
234
|
+
end
|
235
|
+
|
236
|
+
# @param destination [String, IPaddress] a destination for assignment using {Scriptroute::IPv4.ip_dst=}
|
237
|
+
# @param type [String] "udp", "tcp", or "ping" to determine what to send.
|
238
|
+
# @return [IPv4] a packet of specified type
|
239
|
+
def packet_creator(destination, type)
|
240
|
+
case type
|
241
|
+
when 'udp'
|
242
|
+
probe = Scriptroute::UDP.new(12)
|
243
|
+
when 'tcp'
|
244
|
+
probe = Scriptroute::TCP.new(0)
|
245
|
+
when 'ping'
|
246
|
+
@seq = @seq ? @seq + 1 : 0
|
247
|
+
probe = Scriptroute::ICMPecho.new(0)
|
248
|
+
# probe.icmp_type = Scriptroute::Icmp::ICMP_ECHO
|
249
|
+
# probe.icmp_code = 0
|
250
|
+
probe.icmp_seq = @seq
|
251
|
+
else
|
252
|
+
raise "unknown probe type #{type}"
|
253
|
+
end
|
254
|
+
probe.ip_dst = destination
|
255
|
+
probe
|
256
|
+
end
|
257
|
+
|
258
|
+
# @param [Array] id_array an array of IP IDs to check whether unique (duplicates suggest non-aliases.)
|
259
|
+
# @return [Boolean] whether the id_array is unique
|
260
|
+
def unique_ids(id_array)
|
261
|
+
id_array.uniq.length == id_array.length
|
262
|
+
end
|
263
|
+
|
264
|
+
# Handles the case where one or both of the candidate
|
265
|
+
# addresses are unresponsive, thus, Ally won't work.
|
266
|
+
def unresponsive(*all)
|
267
|
+
if all.length > 1 then
|
268
|
+
try_undns "two unresponsive: #{all.join(' and ')}"
|
269
|
+
else
|
270
|
+
try_undns "one unresponsive: #{all.join(' and ')}"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
public
|
275
|
+
|
276
|
+
# create an object that will represent our attempt to test
|
277
|
+
# two addresses. the parameters may be hostnames instead of
|
278
|
+
# addresses.
|
279
|
+
# @param ip_a [String, IPaddress] The first address to test if an alias for the second.
|
280
|
+
# @param ip_b [String, IPaddress] The second address to test if an alias for the first.
|
281
|
+
# @param type [String] "udp", "tcp" (not useful?), or "icmp"
|
282
|
+
def initialize(ip_a, ip_b, type='udp')
|
283
|
+
@a = packet_creator(ip_a, type)
|
284
|
+
@b = packet_creator(ip_b, type)
|
285
|
+
|
286
|
+
# for now, we don't know.
|
287
|
+
@verdict = "UNKNOWN: Failed to complete"
|
288
|
+
|
289
|
+
# we'll throw :resolved when we've figured it out; this is
|
290
|
+
# to simplify the if/elsif/elsif insanity
|
291
|
+
catch :resolved do
|
292
|
+
|
293
|
+
# the quick test; handling this test through the packet
|
294
|
+
# test causes confusion. It is after packet construction
|
295
|
+
# so that the names are looked up to addresses
|
296
|
+
if( @a.ip_dst == @b.ip_dst ) then
|
297
|
+
is_alias "trivial, #{@a.ip_dst} = #{@b.ip_dst}"
|
298
|
+
end
|
299
|
+
|
300
|
+
## this is entirely too complicated, and needs a rewrite.
|
301
|
+
packets = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@a),
|
302
|
+
Struct::DelayedPacket.new(0.001,@b) ])
|
303
|
+
|
304
|
+
## try again if we had a pcap overload style problem
|
305
|
+
if(packets.length < 2 ||
|
306
|
+
packets[0] == nil || packets[0].probe == nil ||
|
307
|
+
packets[1] == nil || packets[1].probe == nil) then
|
308
|
+
packets = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@a),
|
309
|
+
Struct::DelayedPacket.new(0.001,@b) ])
|
310
|
+
if(packets.length < 2 ||
|
311
|
+
packets[0] == nil || packets[0].probe == nil ||
|
312
|
+
packets[1] == nil || packets[1].probe == nil) then
|
313
|
+
try_undns "Internal error: #{@a.ip_dst} and #{@b.ip_dst}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
if(packets[0].response && packets[1].response) then
|
318
|
+
|
319
|
+
assert_response_non_bogus(packets[0])
|
320
|
+
assert_response_non_bogus(packets[1])
|
321
|
+
|
322
|
+
id0 = packets[0].response.packet.ip_id
|
323
|
+
id1 = packets[1].response.packet.ip_id
|
324
|
+
|
325
|
+
if(packets[0].response.packet.ip_src ==
|
326
|
+
packets[1].response.packet.ip_src) then
|
327
|
+
is_alias "mercator/source address: #{merc(packets[0])} #{merc(packets[1])}"
|
328
|
+
|
329
|
+
elsif( id0 == id1 ) then
|
330
|
+
# when they're the same, it's either:
|
331
|
+
if ( id0 == 0 ) then
|
332
|
+
# a) a lack of implementation
|
333
|
+
try_undns "IPIDs not used: both are zero"
|
334
|
+
else
|
335
|
+
# b) not aliases.
|
336
|
+
not_alias "Same IPID."
|
337
|
+
end
|
338
|
+
|
339
|
+
elsif(before(id0-10, id1) && before(id1, id0+200)) then
|
340
|
+
# adding a delay here (the 0.40) seems to increase the likelihood
|
341
|
+
# of a response.
|
342
|
+
packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0.40,@b),
|
343
|
+
Struct::DelayedPacket.new(0.001,@a) ])
|
344
|
+
if(packetz[0] == nil || packetz[1] == nil) then
|
345
|
+
packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0.40,@b),
|
346
|
+
Struct::DelayedPacket.new(0.001,@a) ])
|
347
|
+
if(packetz[0] == nil || packetz[1] == nil) then
|
348
|
+
raise "couldn't send the second set of packets"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
if(packetz[0].response && packetz[1].response) then
|
352
|
+
id2 = packetz[0].response.packet.ip_id
|
353
|
+
id3 = packetz[1].response.packet.ip_id
|
354
|
+
assert_response_non_bogus(packetz[0])
|
355
|
+
assert_response_non_bogus(packetz[1])
|
356
|
+
if(before(id2-10, id3) &&
|
357
|
+
before(id3, id2+200) &&
|
358
|
+
before(id0, id2) &&
|
359
|
+
before(id1, id3) &&
|
360
|
+
unique_ids( [ id0, id1, id2, id3 ] ) ) then
|
361
|
+
is_alias "ally/ipid: #{[id0, id1, id2, id3].join(', ')}"
|
362
|
+
else
|
363
|
+
not_alias "disparate ids: #{[id0, id1, id2, id3].join(', ')}"
|
364
|
+
end
|
365
|
+
elsif(packetz[0].response || packetz[1].response) then
|
366
|
+
last = packetz[ (packetz[0].response) ? 0 : 1 ]
|
367
|
+
|
368
|
+
assert_response_non_bogus(last)
|
369
|
+
id2 = last.response.packet.ip_id
|
370
|
+
if(before(id0, id2) &&
|
371
|
+
before(id1, id2) &&
|
372
|
+
unique_ids( [ id0, id1, id2 ] )) then
|
373
|
+
is_alias "ally/ipid; less response: #{[id0, id1, id2].join(', ')}"
|
374
|
+
else
|
375
|
+
not_alias "disparate ids (3): #{[id0, id1, id2].join(', ')}"
|
376
|
+
end
|
377
|
+
else
|
378
|
+
is_alias "ally/ipid; presumptive (second round had no responses): #{[id0, id1].join(', ')}"
|
379
|
+
end
|
380
|
+
else
|
381
|
+
not_alias "quick (2): #{[id0, id1].join(', ')}"
|
382
|
+
## #{[id0, id1].map {|v| (((v&0xff)*256) + v/256)}.join(', ')} "
|
383
|
+
end
|
384
|
+
elsif(packets[0].response || packets[1].response) then
|
385
|
+
first = packets[ (packets[0].response) ? 0 : 1 ]
|
386
|
+
|
387
|
+
# we received only one response.
|
388
|
+
# try sending again, reordered.
|
389
|
+
packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@b),
|
390
|
+
Struct::DelayedPacket.new(0.001,@a) ])
|
391
|
+
if(packetz[0].response || packetz[1].response) then
|
392
|
+
# we received at least one response
|
393
|
+
second = packetz[ (packetz[0].response) ? 0 : 1 ]
|
394
|
+
assert_response_non_bogus(second)
|
395
|
+
if((second.probe.packet.ip_dst != second.response.packet.ip_src ||
|
396
|
+
first.probe.packet.ip_dst != first.response.packet.ip_src) &&
|
397
|
+
first.response.packet.ip_src == second.response.packet.ip_src) then
|
398
|
+
# shows the signature of a cisco ( responds with a different source address )
|
399
|
+
if(second.probe.packet.ip_dst != first.probe.packet.ip_dst) then
|
400
|
+
# and responses to two different requests
|
401
|
+
# puts second.response.packet
|
402
|
+
# puts first.response.packet
|
403
|
+
is_alias "mercator/source address rate limited: #{merc(first)} #{merc(second)}"
|
404
|
+
else
|
405
|
+
# the destination of both probes we got answers to was the same.
|
406
|
+
# the other destination was unresponsive.
|
407
|
+
unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
|
408
|
+
end
|
409
|
+
else
|
410
|
+
# not necessarily true? might have just lost the first packet in the
|
411
|
+
# first round.
|
412
|
+
unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
|
413
|
+
end
|
414
|
+
else
|
415
|
+
unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
|
416
|
+
end
|
417
|
+
else
|
418
|
+
unresponsive(@a.ip_dst, @b.ip_dst)
|
419
|
+
end
|
420
|
+
fail "shouldn't get here under any circumstances."
|
421
|
+
end # catch.
|
422
|
+
end
|
423
|
+
|
424
|
+
# @return [String] the "verdict"
|
425
|
+
def to_s
|
426
|
+
@verdict
|
427
|
+
end
|
428
|
+
|
429
|
+
# @return [Boolean] whether the "verdict" is "ALIAS", ignoring the explanation.
|
430
|
+
def is?
|
431
|
+
# redundancy is for testing. want to be able to compare
|
432
|
+
# true == true.
|
433
|
+
true & (@verdict =~ /^ALIAS/)
|
434
|
+
end
|
435
|
+
|
436
|
+
# Checks the cache, and if no entry is present, creates a
|
437
|
+
# new alias resolution test object to probe.
|
438
|
+
# @return [Boolean] whether the "verdict" is "ALIAS",
|
439
|
+
# ignoring the explanation.
|
440
|
+
def Ally.aliases?(ip_a, ip_b)
|
441
|
+
key = Ally.to_key(ip_a, ip_b)
|
442
|
+
if(!@@result_cache.has_key?(key)) then
|
443
|
+
Ally.new(ip_a, ip_b)
|
444
|
+
end
|
445
|
+
@@result_cache[key] =~ /^ALIAS/
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# base class of command options. all options must have tags
|
2
|
+
# and a helpful description... or at least a description.
|
3
|
+
class CommandoOpt
|
4
|
+
attr_reader :tags, :description
|
5
|
+
protected
|
6
|
+
def initialize(tags, description)
|
7
|
+
raise ArgumentError, "description must be a string (not #{description.class})" unless(description.is_a?(String))
|
8
|
+
if(!tags.is_a?(Array)) then
|
9
|
+
tags = [tags]
|
10
|
+
end
|
11
|
+
@tags = tags
|
12
|
+
@description = description
|
13
|
+
end
|
14
|
+
public
|
15
|
+
# @return [String] the help text for this option,
|
16
|
+
# comprising tags, default, and description
|
17
|
+
def help
|
18
|
+
" %20s %4s %-72s" % [ tags.join(", "),
|
19
|
+
self.string_default,
|
20
|
+
@description ]
|
21
|
+
end
|
22
|
+
# @note Modifies argument (removing parsed options)
|
23
|
+
# @param [Array<String>] argv the arguments to parse.
|
24
|
+
def seek(argv)
|
25
|
+
argv.each_with_index { |a,i|
|
26
|
+
tags.each { |t|
|
27
|
+
if(a == t) then
|
28
|
+
argv.delete_at(i)
|
29
|
+
if(takes_argument?) then
|
30
|
+
set(argv[i])
|
31
|
+
argv.delete_at(i)
|
32
|
+
else
|
33
|
+
set(TRUE) # if closure, then the arg is ignored.
|
34
|
+
end
|
35
|
+
end
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# simple class for setting variables in command processing.
|
42
|
+
class CommandoVar < CommandoOpt
|
43
|
+
# @return [Symbol] the name of the variable to set
|
44
|
+
attr_reader :varname
|
45
|
+
# @return [Object] the default value of the variable
|
46
|
+
attr_reader :default
|
47
|
+
|
48
|
+
# @return [Boolean] whether the option takes a parameter,
|
49
|
+
# based on whether the default is true or false.
|
50
|
+
def takes_argument?
|
51
|
+
# not really right, it should check the arity of the
|
52
|
+
# closure if present.
|
53
|
+
!(@default == true || @default == false)
|
54
|
+
end
|
55
|
+
# @return [String] the default argument, if any, or an
|
56
|
+
# empty string if no arguments are taken.
|
57
|
+
def string_default
|
58
|
+
if takes_argument? then
|
59
|
+
@default.to_s
|
60
|
+
else
|
61
|
+
""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# basic assignment, if it's a string, we quote it, else
|
66
|
+
# the interpreter should deal with numbers, and complain
|
67
|
+
# if something should have been quoted. This operation
|
68
|
+
# allows crazy s**t to happen, so don't use this in setuid code.
|
69
|
+
#
|
70
|
+
# @param argument [String] the value that the variable
|
71
|
+
# should take, either from the default or from the
|
72
|
+
# command line.
|
73
|
+
# @return [Fixnum,String] likely the argument as
|
74
|
+
# interpreted by eval. Not likely to be useful.
|
75
|
+
def set(argument)
|
76
|
+
Kernel.eval( if(@default.is_a?(String)) then
|
77
|
+
"#{@varname} = \"#{argument}\""
|
78
|
+
else
|
79
|
+
"#{@varname} = #{argument}"
|
80
|
+
end
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param tags [Array<String>,String] the options to
|
85
|
+
# recognize, either just a string "--option" or an array [
|
86
|
+
# "-o", "--option" ]
|
87
|
+
# @param description [String] help text to describe this option.
|
88
|
+
# @param varname [Symbol] a global symbol to set to the
|
89
|
+
# value, e.g., ":$Option"
|
90
|
+
# @param default [Object] a default value for the option,
|
91
|
+
# if not set, useful for the help description. If the default
|
92
|
+
# is not a string, the assignment evals the variable unquoted.
|
93
|
+
# (That is, integer arguments should work as numbers without
|
94
|
+
# a need to convert.)
|
95
|
+
def initialize(tags, description, varname, default=nil)
|
96
|
+
# should probably ensure that this isn't running setuid.
|
97
|
+
super(tags, description)
|
98
|
+
@varname = varname
|
99
|
+
@default = default
|
100
|
+
set(@default)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# instead of setting a variable, execute an anonymous function.
|
105
|
+
# if the function takes an arg, gobble the next command line
|
106
|
+
# argument. If an argument is taken, the function is responsible
|
107
|
+
# for converting the string to whatever type is desired, and
|
108
|
+
# validating the input -- raising an exception if a failure occurs.
|
109
|
+
class CommandoClosure < CommandoOpt
|
110
|
+
attr_reader :closure
|
111
|
+
# @return [Boolean] whether the option takes a parameter
|
112
|
+
def takes_argument?
|
113
|
+
(@closure.arity == -2) # means it takes one arg.
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [String] the default value, mostly for
|
117
|
+
# compatibility with CommandoVar.
|
118
|
+
def string_default
|
119
|
+
if takes_argument? then
|
120
|
+
"[x]" # return something to appease the help msg.
|
121
|
+
else
|
122
|
+
""
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# basic assignment, if it's a string, we quote it, else
|
127
|
+
# the interpreter should deal with numbers, and complain
|
128
|
+
# if something should have been quoted. This operation
|
129
|
+
# allows crazy s**t to happen, so don't use this in setuid code.
|
130
|
+
#
|
131
|
+
# @param argument [String] the value that the variable
|
132
|
+
# should take, either from the default or from the
|
133
|
+
# command line.
|
134
|
+
def set(argument)
|
135
|
+
case @closure.arity
|
136
|
+
when -1
|
137
|
+
@closure.call
|
138
|
+
when -2
|
139
|
+
@closure.call argv[i]
|
140
|
+
argv.delete_at(i)
|
141
|
+
else
|
142
|
+
raise "strange closure takes #{@closure.arity} args"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# @param tags [Array<String>,String] the options to
|
147
|
+
# recognize, either just a string "--option" or an array [
|
148
|
+
# "-o", "--option" ]
|
149
|
+
# @param description [String] help text to describe this option.
|
150
|
+
# @param closure [Proc] the method to invoke if this option is
|
151
|
+
# seen; the number of parameters to the Proc determines how
|
152
|
+
# many subsequent arguments are considered parameters for this
|
153
|
+
# option.
|
154
|
+
def initialize(tags, description, closure)
|
155
|
+
raise ArgumentError, "CommandoClosure takes a lambda as the third arg" unless( closure.is_a?(Proc) )
|
156
|
+
# ruby1.8 seemed to use -1/-2 here; ruby1.9 seems to use
|
157
|
+
# 0/1 here. accept em all.
|
158
|
+
raise ArgumentError, "Closure for #{tags} should take zero or one args, not #{closure.arity}" unless(closure.arity == -1 or closure.arity == -2 or closure.arity == 0 or closure.arity == 1)
|
159
|
+
|
160
|
+
super(tags, description)
|
161
|
+
@closure = closure
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# @example
|
166
|
+
# require "scriptroute/commando"
|
167
|
+
#
|
168
|
+
# c = Commando.new(ARGV, # allows substitution by srclient.rb
|
169
|
+
# [ CommandoVar.new( "--start-speed",
|
170
|
+
# "Kbps rate to start" ,
|
171
|
+
# :$StartSpeedKbps, 20),
|
172
|
+
# CommandoVar.new( "--train-length",
|
173
|
+
# "Filler packets in train" ,
|
174
|
+
# :$TrainLength, 20 ),
|
175
|
+
# CommandoVar.new( "--hop",
|
176
|
+
# "Specific hop to study (ttl=hop and hop-1), 0 for e2e" ,
|
177
|
+
# :$Hop, 0 ),
|
178
|
+
# CommandoVar.new( [ "--verbose", "-v" ],
|
179
|
+
# "print gobs of messages",
|
180
|
+
# :$VERBOSE, false ) ],
|
181
|
+
# "destination-host")
|
182
|
+
#
|
183
|
+
# raise "must start with a positive rate" if($StartSpeedKbps <= 0)
|
184
|
+
# if(ARGV[0] == nil) then
|
185
|
+
# c.usage
|
186
|
+
# exit
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# Main class for option parsing.
|
190
|
+
#
|
191
|
+
# Limitations:
|
192
|
+
# doesn't currently grok the single letter getopt-style
|
193
|
+
# arguments, so "foo.rb -v -d -u" cannot be expressed as
|
194
|
+
# "foo.rb -vdu".
|
195
|
+
#
|
196
|
+
class Commando
|
197
|
+
# Print a usage message to stdout.
|
198
|
+
# @return [void]
|
199
|
+
def usage()
|
200
|
+
puts "Usage: #{$0} [options] #{@after_options_name}"
|
201
|
+
puts " %18s %4s %-72s" % [ "option", "default", "description" ]
|
202
|
+
puts @options.map { |o|
|
203
|
+
o.help
|
204
|
+
}.join("\n")
|
205
|
+
end
|
206
|
+
# Parse any recognized command line options, removing
|
207
|
+
# anything parsed from ARGV.
|
208
|
+
# @param argv [Array<String>] ARGV as provided by the interpreter.
|
209
|
+
# @param options [Array [CommandoVar] an array of option descriptions
|
210
|
+
# @param after_options_name [String] the description of
|
211
|
+
# what comes after the options parsed.
|
212
|
+
def initialize(argv, options, after_options_name)
|
213
|
+
@options = options
|
214
|
+
@after_options_name = after_options_name
|
215
|
+
helpopt = CommandoClosure.new(["--help", "-h"], "this help",
|
216
|
+
lambda { |a| self.usage; exit 0 })
|
217
|
+
begin
|
218
|
+
# helpopt goes last, in case the programmer wrote
|
219
|
+
# a -h option in.
|
220
|
+
(options + [helpopt]).each { |o| o.seek(argv) }
|
221
|
+
rescue NameError => e
|
222
|
+
puts "Error in command line parsing: #{e}"
|
223
|
+
puts
|
224
|
+
self.usage
|
225
|
+
exit
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|