expect4r 0.0.1.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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