scriptroute 0.4.14
Sign up to get free protection for your applications and to get access to all the features.
- 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
|