netutils 0.1.1

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +4 -0
  7. data/README.md +36 -0
  8. data/Rakefile +6 -0
  9. data/bin/acl +109 -0
  10. data/bin/alaxala-deploy +271 -0
  11. data/bin/config-diff-check +111 -0
  12. data/bin/config-gets +64 -0
  13. data/bin/console +14 -0
  14. data/bin/host-locate-on-demand +102 -0
  15. data/bin/ipaddr-resolv +74 -0
  16. data/bin/ipaddr-resolv.sh +97 -0
  17. data/bin/mac-drop +84 -0
  18. data/bin/mac-nodrop +45 -0
  19. data/bin/port-shutdown +78 -0
  20. data/bin/setup +8 -0
  21. data/lib/netutils.rb +118 -0
  22. data/lib/netutils/arp.rb +28 -0
  23. data/lib/netutils/cli.rb +702 -0
  24. data/lib/netutils/cli/alaxala.rb +121 -0
  25. data/lib/netutils/cli/alaxala/interface.rb +137 -0
  26. data/lib/netutils/cli/alaxala/lldp.rb +166 -0
  27. data/lib/netutils/cli/alaxala/macfib.rb +62 -0
  28. data/lib/netutils/cli/alaxala/showarp.rb +51 -0
  29. data/lib/netutils/cli/alaxala/showroute.rb +86 -0
  30. data/lib/netutils/cli/alaxala/showvrf.rb +46 -0
  31. data/lib/netutils/cli/aruba.rb +15 -0
  32. data/lib/netutils/cli/cisco.rb +45 -0
  33. data/lib/netutils/cli/cisco/cdp.rb +117 -0
  34. data/lib/netutils/cli/cisco/ifsummary.rb +32 -0
  35. data/lib/netutils/cli/cisco/interface.rb +67 -0
  36. data/lib/netutils/cli/cisco/macfib.rb +38 -0
  37. data/lib/netutils/cli/cisco/showarp.rb +27 -0
  38. data/lib/netutils/cli/cisco/showinterface.rb +27 -0
  39. data/lib/netutils/cli/cisco/showroute.rb +73 -0
  40. data/lib/netutils/cli/cisco/showvrf.rb +45 -0
  41. data/lib/netutils/cli/nec.rb +20 -0
  42. data/lib/netutils/cli/nec/lldp.rb +16 -0
  43. data/lib/netutils/cli/paloalto.rb +21 -0
  44. data/lib/netutils/fsm.rb +43 -0
  45. data/lib/netutils/macaddr.rb +51 -0
  46. data/lib/netutils/oncequeue.rb +78 -0
  47. data/lib/netutils/parser.rb +30 -0
  48. data/lib/netutils/rib.rb +80 -0
  49. data/lib/netutils/switch.rb +402 -0
  50. data/lib/netutils/tunnel.rb +8 -0
  51. data/lib/netutils/version.rb +3 -0
  52. data/lib/netutils/vrf.rb +42 -0
  53. data/log/.gitignore +1 -0
  54. data/netutils.gemspec +33 -0
  55. metadata +195 -0
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)).untaint, '/..')
3
+
4
+ require 'netutils'
5
+
6
+ ################################################################################
7
+ def usage
8
+ progname = File.basename($0)
9
+ STDERR.print "\
10
+ Usage: ruby #{progname} [-h] [-d] <IP address>
11
+ Options:
12
+ -d: dry run (do not shut down a port, just locate a host only)
13
+ -h: output this help message.
14
+ Example:
15
+ #{progname} 192.168.0.1
16
+ "
17
+ exit
18
+ end
19
+
20
+ case ARGV.size
21
+ when 1
22
+ usage if ARGV[0] !~ /^[0-9\.]+$/
23
+ when 2
24
+ usage if ARGV.shift != '-d'
25
+ dry = true
26
+ else
27
+ usage
28
+ end
29
+ usage if ARGV.size != 1
30
+
31
+ ia = ARGV[0]
32
+
33
+ swname = swia = xia = ma = 'unknown'
34
+ begin
35
+ log "locate directly connected router for #{ia}... "
36
+ sw, xia = router_locate(ia)
37
+ swname = sw.name
38
+ swia = sw.ia
39
+ if ia == xia
40
+ log "\t\"#{sw.name}\" (#{sw.ia})"
41
+ else
42
+ log "\t\"department router\" (#{xia})"
43
+ end
44
+
45
+ log_without_newline "resolving MAC address for #{xia}... "
46
+ ma, vrf, interface = sw.macaddr_resolve(xia)
47
+ log "found"
48
+ log "\t#{xia} #{ma} on VRF \"#{vrf.name}\" #{interface}"
49
+
50
+ vlan = interface_name_vlan_id(interface)
51
+ log_without_newline "setting to \"#{sw.name}\" (#{sw.ia})\n"
52
+
53
+ if dry
54
+ log 'skip (due to -d, dry run, option)'
55
+ exit 0
56
+ else
57
+ sw.configure
58
+
59
+ for vid in vlans_by_switch_name(swname, vlan) do
60
+ cmd = "mac-address-table static #{ma} vlan #{vid} drop"
61
+ log_without_newline "\t#{cmd}"
62
+ sw.cmd(cmd)
63
+ end
64
+
65
+ sw.unconfigure
66
+ log 'done'
67
+
68
+ s = File.join(File.dirname(__FILE__), 'mac-no-drop.rb')
69
+ s = File.expand_path(s)
70
+ log "please run below command on recovery:\n"
71
+ log "\t#{s} #{sw.name} #{sw.ia} #{ma} #{vlan}\n"
72
+ end
73
+ exitcode = 0
74
+ rescue => e
75
+ r = ' FAILED'
76
+ log "\n#{r}: #{e.to_s}"
77
+ log "ERROR: Cannot MAC DROP #{xia} (#{ma}) on #{swname} #{swia} for " +
78
+ "#{ia}"
79
+ exitcode = 1
80
+ end
81
+
82
+ mail "MAC DROP#{r}: #{swname} (#{swia}) for #{xia} (#{ma})", log_buffer
83
+
84
+ exit exitcode
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)).untaint, '/..')
3
+
4
+ require 'netutils'
5
+
6
+ ################################################################################
7
+ def usage
8
+ progname = File.basename($0)
9
+ STDERR.print "\
10
+ Usage: #{progname} <Switch name> <IP address> <MAC address> <existing VLAN>
11
+ Example:
12
+ #{progname} hoge-cisco-01 192.168.0.1 de:ad:be:ef:de:ad
13
+ "
14
+ exit
15
+ end
16
+
17
+ def mac_nodrop(name, ia, mac, vlan)
18
+ sw = Switch.new(name, ia)
19
+ sw.login
20
+ sw.configure
21
+ for vid in vlans_by_switch_name(name, vlan) do
22
+ cmd = "no mac-address-table static #{mac} vlan #{vid}"
23
+ log_without_newline "\t#{cmd}\n"
24
+ sw.cmd(cmd)
25
+ end
26
+ sw.unconfigure
27
+ end
28
+
29
+ usage if ARGV.size != 4
30
+ name = ARGV[0]
31
+ ia = ARGV[1]
32
+ mac = ARGV[2]
33
+ vlan = ARGV[3]
34
+
35
+ begin
36
+ log_without_newline "setting to \"#{name}\"(#{ia})\n\n"
37
+ mac_nodrop(name, ia, mac, vlan)
38
+ log "\nRe-enable #{mac} on #{name} #{ia}"
39
+ rescue => e
40
+ r = ' FAILED'
41
+ log e.to_s
42
+ log "\ncannot allow traffic #{mac} on #{name} #{ia}"
43
+ end
44
+
45
+ mail "MAC NODROP#{r}: #{name} #{ia} #{mac}", log_buffer
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)).untaint, '/..')
3
+
4
+ require 'netutils'
5
+
6
+ ################################################################################
7
+ def usage
8
+ STDERR.print "\
9
+ Usage:
10
+ #{$progname} [-d] (up|down) <Switch IP address> <port>
11
+ Example:
12
+ #{$progname} up 192.168.0.1 GigabitEthernet 1/2/3
13
+ #{$progname} down 192.168.0.2 GigabitEthernet 1/2/3
14
+ "
15
+ exit 1
16
+ end
17
+
18
+ #
19
+ if ARGV[0] === '-d'
20
+ ARGV.shift
21
+ dry = true
22
+ end
23
+ usage if ARGV.size < 3
24
+
25
+ #
26
+ cmd = ARGV.shift
27
+ ia = ARGV.shift
28
+ port = ARGV.shift
29
+ while ARGV.size > 0
30
+ port += ' ' + ARGV.shift
31
+ end
32
+
33
+ #
34
+ case cmd
35
+ when 'up'
36
+ msg = 'bringing up'
37
+ when 'down'
38
+ msg = 'shutting down'
39
+ else
40
+ usage
41
+ end
42
+
43
+ name = 'unknown'
44
+ begin
45
+ log_without_newline "Connecting to #{ia}... "
46
+ sw = Switch.new(nil, Switch::Type::ROUTER, ia)
47
+ sw.login
48
+ log 'done'
49
+ name = sw.name
50
+
51
+ port = sw.interface_name(port)
52
+ interface_sanity_check(name, port)
53
+ log_without_newline "#{msg.capitalize} #{port} on #{name} (#{ia})... "
54
+
55
+ if dry
56
+ log 'skip (due to -d, dry run, option)'
57
+ exit 0
58
+ elsif cmd === 'down'
59
+ sw.interface_shutdown(port)
60
+ s = File.expand_path(__FILE__)
61
+ log 'done'
62
+ log "please run below command on recovery:\n"
63
+ log "\t#{s} up #{ia} \'#{port}\'\n"
64
+ else
65
+ sw.interface_noshutdown(port)
66
+ log 'done'
67
+ end
68
+ exitcode = 0
69
+ rescue => e
70
+ r = ' FAILED'
71
+ log "\n#{r}: #{e.to_s}"
72
+ log "ERROR: Cannot #{msg} #{port} on #{name} #{ia}"
73
+ exitcode = 1
74
+ end
75
+
76
+ mail "Port #{cmd.upcase}#{r}: #{name} #{ia} #{port}", log_buffer
77
+
78
+ exit exitcode
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,118 @@
1
+ require "netutils/version"
2
+ require 'mail'
3
+ require 'netutils/switch'
4
+ require 'config/config'
5
+ require 'config/passwd'
6
+
7
+ module Netutils
8
+
9
+ #
10
+ $progname = File.basename($0)
11
+
12
+ #
13
+ ACL_MAX_SEQ = 4294967294
14
+ #
15
+ $log = ''
16
+
17
+ def mail(s, b)
18
+ m = Mail.new do
19
+ delivery_method :smtp, address: MAILSERVER
20
+ from MAILFROM
21
+ to MAILTO
22
+ subject s
23
+ body b
24
+ end
25
+ m.charset = 'ascii'
26
+ m.deliver!
27
+ end
28
+
29
+ def valid_ip_address?(s)
30
+ return false if s !~ /^(?:[0-9]+\.){3}[0-9]+$/
31
+ results = s.split('.').collect { |i| i.to_i.between?(0, 255) }
32
+ return results.count(true) === 4
33
+ end
34
+
35
+ def interface_sanity_check(host, port)
36
+ # XXX: need more checks to detect backbone links.
37
+ if port !~ /^[gf]/i
38
+ raise "Suppress shutdown #{port}, which may be backbone link."
39
+ end
40
+ end
41
+
42
+ def log_without_newline(m)
43
+ print m
44
+ $log += m
45
+ end
46
+
47
+ def log(m)
48
+ puts m
49
+ $log += m + "\n"
50
+ end
51
+
52
+ def log_buffer
53
+ return $log
54
+ end
55
+
56
+ def vlans_by_switch_name(swname, vlan)
57
+ vlan = vlan.to_i
58
+ vlans = []
59
+ vlans = VLANS[swname].dup if VLANS.has_key?(swname)
60
+ vlans.unshift(vlan) if vlan && ! vlans.include?(vlan)
61
+ return vlans
62
+ end
63
+
64
+ def interface_name_vlan_id(name)
65
+ return $1 if name =~ /^vlan([0-9]+)$/i
66
+ nil
67
+ end
68
+
69
+ def static_neighbor_resolve(name, ifname)
70
+ key = "#{name}_#{ifname}"
71
+ n = STATIC_NEIGHBOR[key]
72
+ return nil if n.nil?
73
+ Switch.get(n[:name], Switch::Type::ROUTER, nil, nil, nil, n[:ia])
74
+ end
75
+
76
+ def tunnel_nexthop_resolve(sw, rt)
77
+ return rt.nh if rt.nh
78
+ c = CDP.new(nil)
79
+ c.parse(sw.cli.cmd("show cdp neighbors #{rt.interface} detail"))
80
+ c.ias[0]
81
+ end
82
+
83
+ def router_locate(ia)
84
+ root = SWITCHES[0]
85
+ sw = Switch.new(root[0], Switch::Type::ROUTER, root[1])
86
+ sw.login
87
+
88
+ while true
89
+ rts = sw.route_gets(ia)
90
+ raise "No route found for #{ia}" if ! rts
91
+ bestrt = nil
92
+ rts.each do |rt|
93
+ case rt.proto
94
+ when 'connected'
95
+ return sw, ia
96
+ when 'static', 'rip', 'ospf', 'bgp'
97
+ # just in case for redistributed routes.
98
+ next if rt.nh === nil && ! rt.tunnel?
99
+ bestrt = rt.compare(bestrt)
100
+ end
101
+ end
102
+ raise "No valid route found for #{ia}" if ! bestrt
103
+
104
+ if OTHER_NEXTHOPS.include?(bestrt.nh)
105
+ return sw, bestrt.nh
106
+ end
107
+
108
+ if bestrt.tunnel?
109
+ nh = tunnel_nexthop_resolve(sw, bestrt)
110
+ else
111
+ nh = bestrt.nh
112
+ end
113
+ sw = Switch.new(nil, Switch::Type::ROUTER, nh)
114
+ sw.login
115
+ end
116
+ end
117
+
118
+ end
@@ -0,0 +1,28 @@
1
+ require 'netutils/macaddr'
2
+
3
+ class ARPTable
4
+ class ARP
5
+ attr_reader :ia, :ma, :interface, :static
6
+
7
+ def initialize(ia, ma, interface, static)
8
+ @ia = ia
9
+ @ma = MACAddr.new(ma)
10
+ @interface = interface
11
+ @static = static
12
+ end
13
+ end
14
+
15
+ attr_reader :arps
16
+
17
+ def initialize
18
+ @arps = {}
19
+ end
20
+
21
+ def add(ia, ma, interface, static)
22
+ @arps[ia] = ARP.new(ia, ma, interface, static)
23
+ end
24
+
25
+ def [](ia)
26
+ return @arps[ia]
27
+ end
28
+ end
@@ -0,0 +1,702 @@
1
+ require 'net/ssh'
2
+ require 'net/ssh/telnet'
3
+ require 'net/telnet'
4
+
5
+ path = File.expand_path(File.dirname(__FILE__)).untaint
6
+ Dir.glob("#{path}/cli/*") do |path|
7
+ file = File.basename(path, '.*')
8
+ require "netutils/cli/#{file}"
9
+ end
10
+
11
+ class CLI
12
+ module Maker
13
+ CISCO = 0
14
+ ALAXALA = 1
15
+ PALOALTO = 2
16
+ ARUBA = 3
17
+ NEC = 4
18
+ #
19
+ # XXX: Cisco WLC should come after Paloalto and NEC for a maker
20
+ # detection by a command to disable a CLI terminal pager.
21
+ # Since Paloalto always interprets a command like:
22
+ #
23
+ # ``config hogehoge''
24
+ #
25
+ # as:
26
+ #
27
+ # ``configure.''
28
+ #
29
+ # WLC pager command is then accepted by Paloalto and a
30
+ # maker detection fails if Cisco WLC preceds Paloalto.
31
+ #
32
+ WLC = 5 # XXX: Cisco WLC, should be product...
33
+ UNKNOWN = 6
34
+ MIN = CISCO
35
+ MAX = ARUBA
36
+ end
37
+
38
+ FIRST_PROMPT_RE = /^.*\n+\(?!?([^\r\n\(\)\s>#]+)\)? ?([>#]) ?\r?\n?.*$/m
39
+
40
+ TIMEOUT = 30
41
+ LOGIN_TIMEOUT = 3
42
+
43
+ attr_reader :name, :ia, :maker, :product, :users, :prompt,
44
+ :passwds, :enable_passwds
45
+
46
+ def initialize(name, ia, types = CLI_SESSION_TYPES, maker = Maker::UNKNOWN)
47
+ @ia = ia
48
+ @users = USERS.dup
49
+ @passwds = PASSWORDS.dup
50
+ @enable_passwds = ENABLES.dup
51
+ @enabled = false
52
+ @type = nil
53
+ @types = types
54
+ @session = nil
55
+ @maker = maker
56
+ @product = nil
57
+ @cr = ''
58
+ @name_supplied = name
59
+ name_update(nil)
60
+ @userprompt = [
61
+ 'login:', # Alaxala
62
+ '[Uu]sername:', # Cisco
63
+ 'User:' # Cisco WiSM/WLC
64
+ ]
65
+ # Cisco has trailing space, ``Password: '', but Alaxala not
66
+ @passwdprompt = 'Password:'
67
+ @telnetopt = Hash.new
68
+ @telnetopt['Timeout' ] = TIMEOUT
69
+ if defined?(LOGDIR)
70
+ path = File.dirname(__FILE__)
71
+ path += "/../"
72
+ path += "/#{LOGDIR}/#{ia}.log"
73
+ path = File.expand_path(path).untaint
74
+ @telnetopt['Output_log'] = path
75
+ end
76
+ #@telnetopt['Dump_log'] = '/dev/stdout'
77
+ end
78
+
79
+ def name_update(name)
80
+ if name =~ /^([^@]*)@(.*)$/
81
+ @user = $1
82
+ name = $2
83
+ end
84
+ raise "Invalid host name given: \"#{name}\"" if name =~ /[>#\n]/
85
+ if @name === nil && name != nil &&
86
+ @name_supplied != nil && name != @name_supplied
87
+ raise(ArgumentError, "host name mismatch: " +
88
+ "\"#{@name_supplied}\" is supplied " +
89
+ "but actually \"#{name}\"")
90
+ end
91
+ @name = name
92
+ case @maker
93
+ when Maker::CISCO
94
+ prefix = suffix = trailer = ''
95
+ when Maker::ALAXALA
96
+ prefix = '!?'
97
+ suffix = ''
98
+ trailer = "\0? "
99
+ when Maker::PALOALTO
100
+ raise('Invalid host name for Paloalto') if ! @user
101
+ prefix = "#{@user}@"
102
+ suffix = ''
103
+ trailer = ' '
104
+ when Maker::ARUBA
105
+ prefix = '\('
106
+ suffix = '\) '
107
+ trailer = ''
108
+ else
109
+ prefix = '\(?'
110
+ if @user
111
+ prefix = "#{prefix}#{@user}@"
112
+ else
113
+ prefix = "#{prefix}.*"
114
+ end
115
+ suffix = '\)? ?'
116
+ trailer = "\0? ?"
117
+ end
118
+ name = '[^\r\n\(\)]+' if ! name
119
+ @normalprompt = "#{prefix}#{name}#{suffix}>#{trailer}"
120
+ @enableprompt = "#{prefix}#{name}#{suffix}##{trailer}"
121
+ @configprompt = "#{prefix}#{name}#{suffix}\\(config[^ ]*\\)##{trailer}"
122
+ if @enabled
123
+ @prompt = @enableprompt
124
+ else
125
+ # XXX configuring node... dirty....
126
+ @prompt = @normalprompt
127
+ end
128
+ end
129
+
130
+ #
131
+ # handle a carriage return (\r) and (\E[K) that intend to
132
+ # remove all characters in a current line and remove trailing
133
+ # characters, respectively.
134
+ #
135
+ def handle_control_characters(input)
136
+ out = ''
137
+ l = ''
138
+ pos = 0
139
+ escape = nil
140
+ input.chars do |c|
141
+ if escape
142
+ escape += c
143
+ case escape
144
+ when /\e[0-9]/, /\e\[[0-9]+;[0-9]+H/, "\e[r"
145
+ escape = nil
146
+ when "\e[K"
147
+ l.slice!(pos, l.length - pos)
148
+ escape = nil
149
+ when "\x1b[6n"
150
+ # XXX: Alaxala edge switch hack...
151
+ escape = nil
152
+ end
153
+ next
154
+ end
155
+ case c
156
+ when "\0"
157
+ # XXX: i do not know but Alaxala sends this...
158
+ when "\b"
159
+ l.chop!
160
+ pos -= 1
161
+ when "\e"
162
+ escape = c
163
+ else
164
+ case c
165
+ when "\r"
166
+ pos = 0
167
+ when "\n"
168
+ #
169
+ # in case of "\r\n", do not override
170
+ # the character.
171
+ #
172
+ pos = l.length if pos === 0
173
+ l[pos] = c
174
+ out += l
175
+ l = ''
176
+ pos = 0
177
+ else
178
+ l[pos] = c
179
+ pos += 1
180
+ end
181
+ end
182
+ end
183
+ out += l if ! l.empty?
184
+ out.delete!("^\u{0001}-\u{007f}")
185
+ return out
186
+ end
187
+ private :handle_control_characters
188
+
189
+ def error?(r)
190
+ case r
191
+ when /%[^\n]+\n+\Z/m # XXX: this is not accurate
192
+ maker = Maker::CISCO
193
+ when /Error: Bad command\. \n\Z/m,
194
+ /[^\s]+: not found\n\Z/m,
195
+ /% The command or parameter at the ^ marker is invalid\./m,
196
+ /Error: Invalid parameter\./m
197
+ maker = Maker::ALAXALA
198
+ when /Invalid syntax\..*\Z/m, /Unknown command: .*\Z/m
199
+ maker = Maker::PALOALTO
200
+ when /^ *\^ *\n% [^\n]+ error/m,
201
+ /^ *\^ *\n% Invalid input detected at/m # same as Cisco
202
+ maker = Maker::ARUBA
203
+ else
204
+ return false
205
+ end
206
+ #maker_update(maker)
207
+ return true
208
+ end
209
+
210
+ def puts(s)
211
+ @session.puts(s + @cr)
212
+ end
213
+ private :puts
214
+
215
+ def cmd(s, nextprompt = nil, ignoreerror = false)
216
+ re = [ @prompt ]
217
+ re.push('#') if @maker == Maker::UNKNOWN # XXX dirty hack for now...
218
+ re.push(nextprompt) if nextprompt
219
+ re = '(?:' + re.join('|') + ')'
220
+ r = @session.cmd('String' => s + @cr, 'Match' => /#{re}\Z/)
221
+ r = handle_control_characters(r)
222
+ #
223
+ # XXX: allows a command not to be echo-ed like FTP server.
224
+ #
225
+ if r !~ /^#{re}?(?:#{s.sub('*', '\\*')})?\n+(.*)#{re}\Z/m
226
+ raise(ArgumentError, "CLI output error: \"#{r}\"")
227
+ end
228
+ r = $1
229
+ r.slice!(-1) if @maker === Maker::ALAXALA && r[-1] === '!'
230
+ if ignoreerror === false && error?(r)
231
+ raise(ArgumentError,
232
+ "Command failed on #{@name}: #{s}: #{r}")
233
+ end
234
+ @prompt = nextprompt if nextprompt
235
+ return r
236
+ end
237
+
238
+ def passwd(users, passwds, opasswds, nextprompt, cmd = nil)
239
+ userpromptre = /(?:#{@userprompt.join('|')})/
240
+ prompts = @userprompt.dup
241
+ prompts << @prompt if @prompt
242
+ prompts << @passwdprompt if @passwdprompt
243
+ prompts << nextprompt if nextprompt
244
+ re = /(?:#{prompts.join('|')})/
245
+ r = @session.waitfor('Match' => re) if users
246
+ while true
247
+ if passwds.empty?
248
+ users.shift if users
249
+ if users && r =~ userpromptre
250
+ passwds = opasswds.dup
251
+ else
252
+ raise(Errno::EPERM,
253
+ 'authentication failed')
254
+ end
255
+ end
256
+ if users && ! users.empty? && r =~ userpromptre
257
+ puts(users[0])
258
+ elsif cmd && r !~ /#{@passwdprompt}/
259
+ puts(cmd)
260
+ end
261
+ if r !~ /#{@passwdprompt}/
262
+ r = @session.waitfor(
263
+ 'Match' => /#{@passwdprompt}/)
264
+ #
265
+ # emulate an exception because net/telnet.rb
266
+ # does not raise an exception when a remote
267
+ # note disconnects after consecutive login
268
+ # failures.
269
+ #
270
+ raise(Errno::ECONNRESET) if r === nil
271
+ end
272
+ puts(passwds[0])
273
+ r = @session.waitfor('Match' => re)
274
+ case r
275
+ when userpromptre
276
+ raise('no user for user prompt') if ! users
277
+ when /#{@passwdprompt}/
278
+ when /#{nextprompt}/
279
+ @prompt = nextprompt
280
+ return r
281
+ else
282
+ if @prompt && r =~ /#{@prompt}/ && ! cmd
283
+ raise('invalid state')
284
+ end
285
+ end
286
+ passwds.shift
287
+ end
288
+ end
289
+ private :passwd
290
+
291
+ def login_ssh(ousers, opasswds)
292
+ users = ousers.dup
293
+ passwds = opasswds.dup
294
+ user = users.shift
295
+ begin
296
+ passwd = passwds.shift
297
+ opt = @telnetopt.dup
298
+ ssh = Net::SSH.start(@ia, user,
299
+ :password => passwd,
300
+ :non_interactive => true,
301
+ #:keys => ['/path/to/private_key'],
302
+ #:port => 22,
303
+ :timeout => opt['Timeout']
304
+ )
305
+ opt['Session'] = ssh
306
+
307
+ #
308
+ # XXX: Alaxala edge switch hack...
309
+ #
310
+ # an Alaxala edge switch asks us cursor position
311
+ # using CSI escap sequence, DSR (0x1b[6n), before
312
+ # outputing prompt. Net::SSH::Telnet then does not
313
+ # consider the case where escape sequence is sent
314
+ # right after login. This incurs long delay until
315
+ # Alaxala outputs the first prompt. Here, we send
316
+ # back a dummy cursor position, (1, 1), to Alaxala.
317
+ #
318
+ opt['Prompt'] = /(?:\e\[6n|[$%#>] ?\Z)/
319
+
320
+ msg = ''
321
+ @session = Net::SSH::Telnet.new(opt) { |l| msg += l }
322
+ @session.prompt = /[$%#>] \z/
323
+ if msg =~ /ALAXALA/
324
+ maker_update(Maker::ALAXALA)
325
+ if msg.rindex("\x1b[6n")
326
+ @cr = "\r"
327
+ @session.write("^1;1R")
328
+ msg += @session.waitfor(/[$%#>] \z/)
329
+ end
330
+ end
331
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::Disconnect => e
332
+ if passwds.empty?
333
+ if users.empty?
334
+ raise(Errno::EPERM, "cannot login to " +
335
+ "#{@ia}")
336
+ end
337
+ user = users.shift
338
+ passwds = opasswds.dup
339
+ end
340
+ retry
341
+ rescue Net::SSH::ConnectionTimeout
342
+ raise("cannot login to #{@ia}")
343
+ end
344
+ #
345
+ # XXX: dirty hack
346
+ #
347
+ # some switch, say Alaxala, disconnects immediately
348
+ # after authentication succeeds if many user logged
349
+ # in. Net::SSH::Telnet.new() unfortunately cannot
350
+ # handle such case.
351
+ #
352
+ if @session.nil?
353
+ raise(Errno::ECONNRESET, 'too many users???')
354
+ end
355
+ return msg
356
+ end
357
+ private :login_ssh
358
+
359
+ def login_telnet(users, passwds)
360
+ opt = @telnetopt.dup
361
+ opt['Host'] = @ia
362
+ users = users.dup if users
363
+ passwds = passwds.dup
364
+ opasswds = passwds.dup
365
+ begin
366
+ @session = Net::Telnet::new(opt)
367
+ r = passwd(users, passwds, opasswds, @normalprompt)
368
+ rescue Errno::ECONNRESET
369
+ retry
370
+ end
371
+ return r
372
+ end
373
+ private :login_telnet
374
+
375
+ def login
376
+ users = @users.dup
377
+ passwds = @passwds.dup
378
+ @type = @types[0]
379
+ begin
380
+ r = send("login_#{@type}", users, passwds)
381
+ rescue Errno::ECONNREFUSED => e
382
+ raise e if @type === @types.last
383
+ @type = @types[@types.index(@type) + 1]
384
+ retry
385
+ end
386
+ r = handle_control_characters(r)
387
+ if r =~ FIRST_PROMPT_RE
388
+ name_update($1)
389
+ if $2 === '#'
390
+ @enabled = true
391
+ end
392
+ end
393
+ if r =~ /NEC Corporation.*OpenROUTE.*J\. Noel Chiappa/m
394
+ maker_update(Maker::NEC)
395
+ end
396
+ product_detect
397
+ end
398
+
399
+ PRODUCT_DETECTION_MAXRETRIES = 3
400
+
401
+ # XXX: exclude maker dependencies...
402
+ def product_detect
403
+ pager_disable
404
+ return if @maker === Maker::UNKNOWN
405
+ case @maker
406
+ when Maker::CISCO
407
+ c = 'show version'
408
+ # XXX: Nexus not supported yet
409
+ re = /IOS Software, ([^\s]+) Software .*/m
410
+ when Maker::WLC
411
+ c = 'show sysinfo'
412
+ re = /.*Product Name[^ ]+ (.*)$/m
413
+ when Maker::ALAXALA
414
+ c = 'show version'
415
+ re = /.*Model:\s+(AX[^\s\n]+).*$/m
416
+ when Maker::PALOALTO
417
+ c = 'show system info'
418
+ re = /model: ([^\s\n]+).*/m
419
+ when Maker::ARUBA
420
+ c = 'show version'
421
+ re = /.*ArubaOS \(MODEL: ([^\)]+)\),.*/m
422
+ when Maker::NEC
423
+ c = 'show version'
424
+ re = /^IX Series ([^ ]+) .*$/m
425
+ when Maker::UNKNOWN
426
+ return
427
+ end
428
+
429
+ leftchance = PRODUCT_DETECTION_MAXRETRIES
430
+ begin
431
+ v = cmd(c)
432
+ if v !~ re
433
+ raise('Unknown maker detected')
434
+ end
435
+ @product = $1
436
+ disable_logging_console
437
+ rescue => e
438
+ leftchance -= 1
439
+ raise e if leftchance === 0
440
+ retry
441
+ end
442
+ end
443
+ private :product_detect
444
+
445
+ def maker_to_s
446
+ # XXX: smarter way please...
447
+ case @maker
448
+ when Maker::CISCO
449
+ 'Cisco'
450
+ when Maker::WLC
451
+ 'WLC'
452
+ when Maker::ALAXALA
453
+ 'Alaxala'
454
+ when Maker::PALOALTO
455
+ 'Paloalto'
456
+ when Maker::ARUBA
457
+ 'Aruba'
458
+ when Maker::NEC
459
+ 'NEC'
460
+ else
461
+ 'Unknown'
462
+ end
463
+ end
464
+
465
+ def yes_or_no(yes_or_no, re)
466
+ re = /\n[^\n]+[Yy]\/[Nn]\)? ?$/m if re == nil
467
+ @session.waitfor('Match' => re)
468
+ puts(yes_or_no)
469
+ end
470
+ private :yes_or_no
471
+
472
+ def yes(re = nil)
473
+ yes_or_no('y', re)
474
+ end
475
+
476
+ def no(re = nil)
477
+ yes_or_no('n', re)
478
+ end
479
+
480
+ def logout
481
+ @session.close
482
+ end
483
+
484
+ def maker_update(maker)
485
+ return if @maker != Maker::UNKNOWN
486
+ @maker = maker
487
+ extend Module.const_get("#{maker_to_s}")
488
+ end
489
+ private :maker_update
490
+
491
+ def pager_disable_cisco
492
+ cmd('terminal length 0')
493
+ end
494
+ private :pager_disable_cisco
495
+
496
+ def pager_disable_wlc
497
+ cmd('config paging disable')
498
+ end
499
+
500
+ def pager_disable_alaxala
501
+ cmd('set terminal pager disable')
502
+ end
503
+ private :pager_disable_alaxala
504
+
505
+ def pager_disable_paloalto
506
+ cmd('set cli pager off')
507
+ end
508
+ private :pager_disable_paloalto
509
+
510
+ def pager_disable_aruba
511
+ enable
512
+ cmd('no paging')
513
+ end
514
+ private :pager_disable_aruba
515
+
516
+ def pager_disable_nec
517
+ configure
518
+ cmd('terminal length 0')
519
+ unconfigure
520
+ end
521
+ private :pager_disable_nec
522
+
523
+ def pager_disable
524
+ maker = omaker = @maker
525
+ maker = Maker::MIN if omaker === Maker::UNKNOWN
526
+ begin
527
+ @maker = maker
528
+ send('pager_disable_' + maker_to_s.downcase)
529
+ rescue Timeout::Error => e
530
+ raise e
531
+ rescue => e
532
+ if omaker === Maker::UNKNOWN
533
+ maker += 1
534
+ if maker != Maker::UNKNOWN &&
535
+ e != ArgumentError
536
+ retry
537
+ else
538
+ @maker = omaker
539
+ raise
540
+ end
541
+ end
542
+ ensure
543
+ @maker = omaker
544
+ end
545
+ maker_update(maker)
546
+ name_update(@name) if omaker === Maker::UNKNOWN
547
+ end
548
+ private :pager_disable
549
+
550
+ def enable
551
+ return if @enabled
552
+ passwd(nil, @enable_passwds.dup, @enable_passwds.dup,
553
+ @enableprompt.dup, 'enable')
554
+ @enabled = true
555
+ end
556
+
557
+ def configure
558
+ raise "already configuring" if @prompt == @configprompt
559
+ enable
560
+ return cmd('configure terminal', @configprompt)
561
+ end
562
+
563
+ def unconfigure
564
+ raise "currently not configuring" if @prompt != @configprompt
565
+ return cmd('end', @enableprompt)
566
+ end
567
+
568
+ def config_get
569
+ enable
570
+ re = Module.const_get(maker_to_s).const_get('CONFIG_RE')
571
+ if show_running_config !~ re
572
+ raise("Invalid configuration format for #{@name}")
573
+ end
574
+ return $1
575
+ end
576
+
577
+ def _new(name, *arg)
578
+ c = Module.const_get(maker_to_s)
579
+ if c.const_defined?(name)
580
+ c.const_get(name).new(*arg)
581
+ else
582
+ nil
583
+ end
584
+ end
585
+ private :_new
586
+
587
+ def route_gets(ia)
588
+ r = _new(:ShowRoute)
589
+ r.parse(cmd(r.cmd(ia), nil, true))
590
+ return r.rib.get(ia)
591
+ end
592
+
593
+ def vrf_gets
594
+ v = _new(:ShowVRF)
595
+ v.parse(cmd(v.cmd))
596
+ v.vrfs.add('default', '0:0') if v.vrfs.empty?
597
+ return v.vrfs
598
+ end
599
+
600
+ def arp_resolve(ia, vrf)
601
+ if vrf.name == 'default'
602
+ output = cmd("show ip arp #{ia}")
603
+ else
604
+ output = cmd("show ip arp vrf #{vrf.name} #{ia}")
605
+ end
606
+ a = _new(:ShowARP)
607
+ a.parse(output)
608
+ return a.arps[ia]
609
+ end
610
+
611
+ def mac_address_table_get(sw, ma, vlan)
612
+ fib = _new(:MACFIB, sw)
613
+ fib.parse(cmd(fib.cmd(ma, vlan)))
614
+ return fib.ports
615
+ end
616
+
617
+ def interface_gets(sw)
618
+ i = _new(:Interface, sw)
619
+ i.parse(cmd(i.cmd))
620
+ #
621
+ # XXX: hack for Cisco because Cisco cannot obtain up/down of
622
+ # an interface with interfaces capability command...
623
+ #
624
+ is = _new(:IfSummary, sw)
625
+ is.parse(cmd(is.cmd)) if is
626
+ end
627
+
628
+ def interface_name(port)
629
+ port
630
+ end
631
+
632
+ def interface_name_cli(port)
633
+ port
634
+ end
635
+
636
+ def interface_shutdown(port)
637
+ configure
638
+ cmd("interface #{port}")
639
+ cmd('shutdown')
640
+ unconfigure
641
+ end
642
+
643
+ def interface_noshutdown(port)
644
+ configure
645
+ cmd("interface #{port}")
646
+ cmd('no shutdown')
647
+ unconfigure
648
+ end
649
+
650
+ def acl_exists?(type, name)
651
+ configure
652
+ filters = cmd("show #{acl_definition(type, name)}")
653
+ unconfigure
654
+ ! filters.empty?
655
+ end
656
+
657
+ def acl_add(type, name, addr, seq = nil)
658
+ case type
659
+ when 'ip'
660
+ # XXX: we may need sanity check.
661
+ when 'mac', 'advance'
662
+ addr = MACAddr.new(addr)
663
+ else
664
+ raise("Unknown ACL type: #type")
665
+ end
666
+ cmd = acl_type_to_cmd(type)
667
+ configure
668
+ cmd(acl_definition(type, name))
669
+ cmd("#{seq} deny #{cmd} host #{addr} any")
670
+ unconfigure
671
+ end
672
+
673
+ def acl_delete(type, name, seq)
674
+ configure
675
+ cmd(acl_definition(type, name))
676
+ cmd("no #{seq}")
677
+ unconfigure
678
+ end
679
+
680
+ def neighbor_gets(sw, port = nil)
681
+ c = _new(:CDP, sw)
682
+ if c
683
+ begin
684
+ c.parse(cmd(c.cmd(port)))
685
+ return c.rsw if port && c.rsw
686
+ rescue ArgumentError => e
687
+ if e.to_s !~ /% CDP is not enabled/
688
+ raise e
689
+ end
690
+ end
691
+ end
692
+ l = _new(:LLDP, sw)
693
+ if l
694
+ l.parse(cmd(l.cmd(port)))
695
+ return l.rsw if port && l.rsw
696
+ end
697
+ if ! c && ! l
698
+ raise 'No method found for retrieving a neighbor!!'
699
+ end
700
+ nil
701
+ end
702
+ end