expect4r 0.0.1.dev

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,53 @@
1
+ Expect4r is copyrighted free software by Jean-Michel Esnault.
2
+
3
+ You can redistribute it and/or modify it under either the terms of the GPL
4
+ (see the COPYING file), or the conditions below:
5
+
6
+ 1. You may make and give away verbatim copies of the source form of the
7
+ software without restriction, provided that you duplicate all of the
8
+ original copyright notices and associated disclaimers.
9
+
10
+ 2. You may modify your copy of the software in any way, provided that
11
+ you do at least ONE of the following:
12
+
13
+ a) place your modifications in the Public Domain or otherwise
14
+ make them Freely Available, such as by posting said
15
+ modifications to Usenet or an equivalent medium, or by allowing
16
+ the author to include your modifications in the software.
17
+
18
+ b) use the modified software only within your corporation or
19
+ organization.
20
+
21
+ c) rename any non-standard executables so the names do not conflict
22
+ with standard executables, which must also be provided.
23
+
24
+ d) make other distribution arrangements with the author.
25
+
26
+ 3. You may distribute the software in object code or executable
27
+ form, provided that you do at least ONE of the following:
28
+
29
+ a) distribute the executables and library files of the software,
30
+ together with instructions (in the manual page or equivalent)
31
+ on where to get the original distribution.
32
+
33
+ b) accompany the distribution with the machine-readable source of
34
+ the software.
35
+
36
+ c) give non-standard executables non-standard names, with
37
+ instructions on where to get the original software distribution.
38
+
39
+ d) make other distribution arrangements with the author.
40
+
41
+ 4. You may modify and include the part of the software into any other
42
+ software (possibly commercial).
43
+
44
+ 5. The scripts and library files supplied as input to or produced as
45
+ output from the software do not automatically fall under the
46
+ copyright of the software, but belong to whomever generated them,
47
+ and may be sold commercially, and may be aggregated with this
48
+ software.
49
+
50
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
51
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
52
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
53
+ PURPOSE.
data/README.rdoc ADDED
@@ -0,0 +1,126 @@
1
+ Expect4r is a library that enables a ruby program to 'talk' to routers following a written ruby script.
2
+ You talk to routers by connecting to them using ssh or telnet and send exec or config command and collect router output than can be parsed using ruby.
3
+
4
+ * Connect to routers
5
+
6
+ ios = Ios.new_telnet :host=> "hostname", :user=> "username", :pwd => "password"
7
+ ios.login
8
+
9
+
10
+ iox = Iox.new_telnet 'host', 'username', 'password'
11
+ iox.login
12
+
13
+ j = J.new_telnet :host=> '', :user=> 'lab', :pwd => 'lab'
14
+ j.login
15
+
16
+ * Push configigurations to routers
17
+
18
+ ios.config 'no logging console'
19
+
20
+ ios.config %{
21
+ interface loopback0
22
+ shutdown
23
+ }
24
+
25
+
26
+ iox.config
27
+ iox.exp_send 'interface GigabitEthernet0/2/0/0'
28
+ iox.exp_send 'desc to switch port 13'
29
+ iox.exp_send 'ipv4 address 190.0.0.9 255.255.255.252'
30
+ iox.exp_send 'no shut'
31
+ iox.commit
32
+
33
+
34
+ j.config %{
35
+ edit logical-router Orleans protocols bgp
36
+ edit group session-to-200
37
+ set type external
38
+ set peer-as 200
39
+ set neighbor 40.0.2.1 peer-as 200
40
+ }
41
+
42
+ * exec commands
43
+
44
+
45
+ j.exec 'set cli screen-length 0'
46
+
47
+ iox.exec "terminal len 0\nterminal width 0"
48
+
49
+
50
+ * exec shell commands
51
+
52
+ iox.shell 'pidin'
53
+
54
+
55
+ * interact with CLI.
56
+
57
+
58
+ irb> r.interact
59
+
60
+ #
61
+ # ^Z to terminate.
62
+ #
63
+
64
+ router#clear line 2
65
+ [confirm]
66
+ [OK]
67
+ router# ^Z
68
+ => nil
69
+ irb>
70
+
71
+
72
+ irb(main):210:0* j.interact
73
+
74
+ #
75
+ # ^Z to terminate.
76
+ #
77
+
78
+ jme@router> configure
79
+ Entering configuration mode
80
+ The configuration has been changed but not committed
81
+
82
+ [edit]
83
+ jme@router# rollback
84
+ load complete
85
+
86
+ [edit]
87
+ jme@router# show | compare
88
+
89
+ [edit]
90
+ jme@router# ^Z
91
+ => nil
92
+ irb(main):211:0>
93
+
94
+ * Parsing output
95
+
96
+ The output is returned as an array of arrays of responses to commands.
97
+ Each response is returned as an array of lines.
98
+ Parsing a response becomes a matter of iterating an array using the
99
+ Arrays's enumerator methods such as find, find_all ...
100
+
101
+ irb(main):025:0> output = ts.exec %{
102
+ irb(main):026:0" show terminal | include History
103
+ irb(main):027:0" show line | include AUX
104
+ irb(main):028:0" }
105
+ => [
106
+ ["show terminal | include History", "History is enabled, history size is 10.", "PEA#"],
107
+ ["show line | include AUX", " 17 AUX 9600/9600 - 0/0 -", "PEA#"]
108
+ ]
109
+
110
+ output[0] contains output for 1st command 'show terminal'.
111
+ output[1] contains output for 1st command 'show line'.
112
+ ...
113
+ output[n-1] contains output for nth command.
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
data/lib/expect/io.rb ADDED
@@ -0,0 +1,484 @@
1
+ require 'thread'
2
+ require 'pty'
3
+ require 'set'
4
+
5
+ Thread.abort_on_exception=true
6
+
7
+ class IO
8
+ def _io_save(no_echo=false, match_string=nil, ch=nil)
9
+ s = _io_string!
10
+ exp_internal match_string
11
+ exp_internal s
12
+ return if no_echo
13
+ s = s.chomp(ch) if ch
14
+ _io_buf1 << s unless no_echo
15
+ end
16
+ def _io_more? ; ! IO.select([self],nil,nil,0.20).nil? ; end
17
+ def _io_exit? ; _io_buf0.last.nil? ; end
18
+ def _io_buf0 ; @_buf0_ ; end
19
+ def _io_buf1 ; @_buf1_ ; end
20
+ def _io_string ; @_buf0_.join ; end
21
+ def _io_string!
22
+ b = _io_string
23
+ @_buf0_=[]
24
+ b
25
+ end
26
+ def _io_sync
27
+ @_buf0_ ||=[]
28
+ while _io_more?
29
+ break if _io_read_char.nil?
30
+ end
31
+ end
32
+ def _io_read_char
33
+ c = getc
34
+ @_buf0_ << c.chr unless c.nil?
35
+ exp_internal "buf0: [#{_io_string.gsub(/\n/,'\n').gsub(/\r/,'\r')}]"
36
+ c
37
+ end
38
+ def readbuf(ti=10)
39
+ @_buf0_, @_buf1_=[],[]
40
+ loop do
41
+ if IO.select([self],nil,nil,ti).nil?
42
+ exp_internal "IO.select is NIL (TIMEOUT=#{ti})"
43
+ @_buf0_ << nil
44
+ else
45
+ _io_read_char
46
+ end
47
+ yield(self)
48
+ end
49
+ end
50
+ end
51
+
52
+ module Expect4r
53
+
54
+ class ConnectionError < RuntimeError
55
+ def initialize(output)
56
+ @output = output
57
+ end
58
+ def err_msg
59
+ "Connection Error: #{@output}"
60
+ end
61
+ end
62
+
63
+ class ExpTimeoutError < RuntimeError
64
+ def initialize(output, elapsed)
65
+ @output, @elapsed = output, elapsed
66
+ end
67
+ def err_msg
68
+ "Timeout Error: #{@output}"
69
+ end
70
+ end
71
+
72
+ class NoChildError < RuntimeError
73
+ end
74
+ class SpawnError < RuntimeError
75
+ def initialize(cmd)
76
+ @cmd = cmd
77
+ end
78
+ def err_msg
79
+ "Error: #{msg}"
80
+ end
81
+ end
82
+ class Expect4rIO_Error < RuntimeError
83
+ end
84
+
85
+ def child_exit
86
+ if @pid
87
+ Process.kill(:KILL, @pid)
88
+ @thread.kill
89
+ @lp, @r, @w, @pid = [nil]*4
90
+ end
91
+ rescue Errno::ESRCH, Errno::ECHILD => e
92
+ ensure
93
+ @lp, @r, @w, @pid = [nil]*4
94
+ end
95
+
96
+ def spawn(cmd)
97
+ begin
98
+ child_exited = false
99
+ # STDOUT.puts "*** FALSE ***"
100
+ @thread = Thread.new do
101
+ PTY.spawn(cmd) do |pipe_read, pipe_write, pid|
102
+ @r, @w, @pid = pipe_read, pipe_write, pid
103
+ begin
104
+ Process.wait(@pid,0)
105
+ rescue
106
+ ensure
107
+ child_exited = true
108
+ end
109
+ end
110
+ end
111
+ @thread.priority = -2
112
+ # STDOUT.puts "*** #{child_exited} ***"
113
+ unless child_exited
114
+ while @r.nil?
115
+ sleep(0.05)
116
+ end
117
+ end
118
+ rescue => e
119
+ raise SpawnError.new(cmd)
120
+ end
121
+ end
122
+
123
+ def logout
124
+ child_exit
125
+ end
126
+
127
+ def putc(c)
128
+ return unless @w || c.nil?
129
+ exp_internal "[#{c}]"
130
+ @w.putc(c) and flush
131
+ rescue Errno::EIO
132
+ child_exit
133
+ raise
134
+ end
135
+
136
+ def exp_puts(s)
137
+ exp_print "#{s}\r"
138
+ end
139
+ def exp_print(s)
140
+ exp_internal "print: #{s.inspect}, io_writer: #{@w}"
141
+ return unless @w
142
+ @w.print(s) and flush
143
+ rescue Errno::EIO, Errno::ECHILD
144
+ child_exit
145
+ raise
146
+ end
147
+ def getc
148
+ @r.getc if @r
149
+ rescue Errno::EIO, Errno::ECHILD
150
+ child_exit
151
+ raise
152
+ end
153
+
154
+ def interact(k=?\C-z)
155
+ STDOUT.puts "\n\#\n\# #{ctrl_key k} to terminate.\n\#\n"
156
+ reader :start
157
+ writer k
158
+ rescue
159
+ ensure
160
+ begin
161
+ reader :terminate
162
+ putline ' ', :no_trim=>true, :no_echo=>true
163
+ rescue => e
164
+ exp_internal e.to_s
165
+ end
166
+ end
167
+
168
+ def putcr
169
+ putline '', :no_trim=>true, :no_echo=>true
170
+ nil
171
+ end
172
+
173
+ def exp_send(lines, arg={})
174
+ r=[]
175
+ lines.each_line do |l|
176
+ l = l.chomp("\n").chomp("\r").strip
177
+ r << putline(l, arg) if l.size>0
178
+ end
179
+ lines.size==1 ? r[0] : r
180
+ end
181
+
182
+ def expect(match, ti=5)
183
+ ev, buf = catch(:done) do
184
+ @r.readbuf(ti) do |r|
185
+ if r._io_exit?
186
+ throw :done, [ :timeout, r._io_string!]
187
+ end
188
+ case r._io_string
189
+ when match
190
+ throw :done, [:ok, r._io_string!.chomp("\r\n")]
191
+ end
192
+ end
193
+ end
194
+ exp_internal "#{ev.inspect} buf: #{buf.inspect}"
195
+ raise RuntimeError, buf if ev == :timeout
196
+ [buf, ev]
197
+ end
198
+
199
+ def readline(ti=2)
200
+ expect(/(.+)\r\n/, ti)
201
+ end
202
+
203
+ def connected?
204
+ @r && (not child_exited?)
205
+ end
206
+
207
+ def login(c, o={})
208
+ return if connected?
209
+
210
+ spawn c
211
+
212
+ o={:timeout=>13, :no_echo=>false}.merge(o)
213
+ timeout = o[:timeout]
214
+ no_echo = o[:no_echo]
215
+
216
+ output=[]
217
+ t0 = Time.now
218
+ ev, buf = catch(:done) do
219
+ @r.readbuf(timeout) do |read_pipe|
220
+ if read_pipe._io_exit?
221
+ exp_internal "readbuf: _io_exit?"
222
+ throw :done, [ :abort, output]
223
+ end
224
+ case read_pipe._io_string
225
+ when spawnee_prompt
226
+ read_pipe._io_save false, "match PROMPT"
227
+ throw(:done, [:ok, output])
228
+ when /(user\s*name\s*|login):\r*$/i
229
+ read_pipe._io_save no_echo, "match USERNAME"
230
+ exp_puts spawnee_username
231
+ when /password:\s*$/i
232
+ read_pipe._io_save no_echo, "match PASSWORD"
233
+ @w.print(spawnee_password+"\r") and flush
234
+ when /Escape character is/
235
+ read_pipe._io_save no_echo, "match Escape char"
236
+ io_escape_char_cb
237
+ when /.*\r\n/
238
+ exp_internal "match EOL"
239
+ read_pipe._io_save no_echo, "match EOL", "\r\n"
240
+ end
241
+ end
242
+ end
243
+ case ev
244
+ when :abort
245
+ elapsed = Time.now - t0
246
+ if elapsed < timeout
247
+ raise ConnectionError.new(c)
248
+ else
249
+ raise ExpTimeoutError.new(c, elapsed)
250
+ end
251
+ else
252
+ @lp = buf.last
253
+ end
254
+ [buf, ev]
255
+
256
+ end
257
+
258
+ private
259
+
260
+ #FIXME ? putline to send_cmd ?
261
+ # hide putline and expose cmd
262
+ def putline(line, arg={})
263
+ raise NoChildError if child_exited?
264
+
265
+ arg = {:ti=>13, :no_echo=>false, :debug=>0, :sync=> false, :no_trim=>false}.merge(arg)
266
+ no_echo = arg[:no_echo]
267
+ ti = arg[:ti]
268
+ unless arg[:no_trim]
269
+ line = line.gsub(/\s+/,' ').gsub(/^\s+/,'') unless arg[:no_trim]
270
+ return [[], :empty_line] unless line.size>0
271
+ end
272
+ sync if arg[:sync]
273
+ t0 = Time.now
274
+ exp_puts line
275
+ output=[]
276
+ rc, buf = catch(:done) do
277
+ @r.readbuf(arg[:ti]) do |r|
278
+ if r._io_exit?
279
+ r._io_save(no_echo)
280
+ throw :done, [ :abort, r._io_buf1]
281
+ end
282
+ case r._io_string
283
+ when @ps1, @ps1_bis
284
+ unless r._io_more?
285
+ r._io_save false, "matching PROMPT"
286
+ throw(:done, [:ok, r._io_buf1])
287
+ end
288
+ exp_internal "more..."
289
+ when /(.+)\r\n/, "\r\n"
290
+ r._io_save no_echo, "matching EOL", "\r\n"
291
+ when @more
292
+ r._io_save no_echo, "matching MORE"
293
+ putc ' '
294
+ end
295
+
296
+ @matches.each { |match, _send|
297
+ if r._io_string =~ match
298
+ r._io_save no_echo, "match #{match}"
299
+ exp_puts _send
300
+ end
301
+ }
302
+
303
+ end
304
+ end
305
+ case rc
306
+ when :abort
307
+ elapsed = Time.now - t0
308
+ if elapsed < ti
309
+ raise ConnectionError.new(line)
310
+ else
311
+ raise ExpTimeoutError.new(line, elapsed)
312
+ end
313
+ else
314
+ @lp = buf.last
315
+ end
316
+ [buf, rc]
317
+ end
318
+ def ctrl_key(k)
319
+ case k
320
+ when ?\C-c ; '^C'
321
+ when ?\C-q ; '^Q'
322
+ when ?\C-z ; '^Z'
323
+ end
324
+ end
325
+
326
+ def writer(k)
327
+ stty_raw
328
+ begin
329
+ loop do
330
+ break if (c = STDIN.getc) == k
331
+ putc(c)
332
+ end
333
+ rescue PTY::ChildExited => e
334
+ child_exit
335
+ ensure
336
+ stty_cooked
337
+ end
338
+ end
339
+
340
+ def reader(arg=:start)
341
+ case arg
342
+ when :start
343
+ stty_raw
344
+ @reader = Thread.new do
345
+ begin
346
+ loop do
347
+ c = getc
348
+ break if c.nil?
349
+ STDOUT.putc c
350
+ end
351
+ rescue => e
352
+ p e
353
+ p '7777777'
354
+ ensure
355
+ stty_cooked
356
+ end
357
+ end
358
+ when :terminate
359
+ @reader.terminate if @reader
360
+ stty_cooked
361
+ end
362
+ end
363
+
364
+ def sync
365
+ @r._io_sync
366
+ end
367
+
368
+ def child_exited?
369
+ @pid == nil
370
+ end
371
+
372
+ def stty_cooked
373
+ system "stty echo -raw"
374
+ end
375
+
376
+ def stty_raw
377
+ system "stty -echo raw"
378
+ end
379
+
380
+ def flush
381
+ @w.flush
382
+ end
383
+
384
+ end
385
+
386
+ module Kernel
387
+ def exp_debug(state=:enable)
388
+ case state
389
+ when :disable, :off
390
+ Kernel.instance_eval {
391
+ define_method("exp_internal") do |s|
392
+ end
393
+ }
394
+ :disable
395
+ when :enable, :on
396
+ Kernel.instance_eval {
397
+ define_method("exp_internal") do |s|
398
+ STDOUT.print "\ndebug: #{s}"
399
+ end
400
+ }
401
+ :enable
402
+ else
403
+ nil
404
+ end
405
+ end
406
+ exp_debug :disable
407
+ end
408
+
409
+ module Expect4r
410
+ class BaseObject
411
+ class << self
412
+ def new_telnet(*args)
413
+ new :telnet, *args
414
+ end
415
+ def new_ssh(*args)
416
+ new :ssh, *args
417
+ end
418
+ attr_reader :routers
419
+ def add(r)
420
+ @routers ||=[]
421
+ @routers << r
422
+ end
423
+ end
424
+ attr_reader :host, :user, :method, :port
425
+ alias :username :user
426
+ alias :hostname :host
427
+ def initialize(*args)
428
+ ciphered_pwd=nil
429
+ if args.size>2 and args[1].is_a?(String)
430
+ @method, host, @user, pwd = args
431
+ elsif args.size == 2 and args[1].is_a?(Hash) and args[0].is_a?(Symbol)
432
+ @method = args[0]
433
+ host = args[1][:host] || args[1][:hostname]
434
+ @user = args[1][:user]|| args[1][:username]
435
+ pwd = args[1][:pwd] || args[1][:password]
436
+ ciphered_pwd = args[1][:ciphered_pwd]
437
+ end
438
+ @host, port = host.split
439
+ @port = port.to_i
440
+ @pwd = if ciphered_pwd
441
+ ciphered_pwd
442
+ else
443
+ Expect4r.cipher(pwd) if pwd
444
+ end
445
+ @ps1 = /(.*)(>|#|\$)\s*$/
446
+ @more = / --More-- /
447
+ @matches=Set.new
448
+ BaseObject.add(self)
449
+ self
450
+ end
451
+ end
452
+ end
453
+
454
+ if __FILE__ != $0
455
+
456
+ at_exit {
457
+ if Expect4r::BaseObject.routers
458
+ Expect4r::BaseObject.routers.each { |r| r.logout if r.respond_to? :logout }
459
+ end
460
+ }
461
+
462
+ else
463
+
464
+ require "test/unit"
465
+
466
+ class Expect4r::BaseObject
467
+ def initialize
468
+ Expect4r::BaseObject.add(self)
469
+ end
470
+ end
471
+
472
+ class TestRouterBaseObject < Test::Unit::TestCase
473
+ include Expect4r
474
+
475
+ def test_add
476
+ assert [], BaseObject.routers
477
+ BaseObject.new
478
+ assert 1, BaseObject.routers.size
479
+ BaseObject.new
480
+ assert_equal 2, BaseObject.routers.size
481
+ end
482
+ end
483
+
484
+ end