netutils 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/bin/acl +109 -0
- data/bin/alaxala-deploy +271 -0
- data/bin/config-diff-check +111 -0
- data/bin/config-gets +64 -0
- data/bin/console +14 -0
- data/bin/host-locate-on-demand +102 -0
- data/bin/ipaddr-resolv +74 -0
- data/bin/ipaddr-resolv.sh +97 -0
- data/bin/mac-drop +84 -0
- data/bin/mac-nodrop +45 -0
- data/bin/port-shutdown +78 -0
- data/bin/setup +8 -0
- data/lib/netutils.rb +118 -0
- data/lib/netutils/arp.rb +28 -0
- data/lib/netutils/cli.rb +702 -0
- data/lib/netutils/cli/alaxala.rb +121 -0
- data/lib/netutils/cli/alaxala/interface.rb +137 -0
- data/lib/netutils/cli/alaxala/lldp.rb +166 -0
- data/lib/netutils/cli/alaxala/macfib.rb +62 -0
- data/lib/netutils/cli/alaxala/showarp.rb +51 -0
- data/lib/netutils/cli/alaxala/showroute.rb +86 -0
- data/lib/netutils/cli/alaxala/showvrf.rb +46 -0
- data/lib/netutils/cli/aruba.rb +15 -0
- data/lib/netutils/cli/cisco.rb +45 -0
- data/lib/netutils/cli/cisco/cdp.rb +117 -0
- data/lib/netutils/cli/cisco/ifsummary.rb +32 -0
- data/lib/netutils/cli/cisco/interface.rb +67 -0
- data/lib/netutils/cli/cisco/macfib.rb +38 -0
- data/lib/netutils/cli/cisco/showarp.rb +27 -0
- data/lib/netutils/cli/cisco/showinterface.rb +27 -0
- data/lib/netutils/cli/cisco/showroute.rb +73 -0
- data/lib/netutils/cli/cisco/showvrf.rb +45 -0
- data/lib/netutils/cli/nec.rb +20 -0
- data/lib/netutils/cli/nec/lldp.rb +16 -0
- data/lib/netutils/cli/paloalto.rb +21 -0
- data/lib/netutils/fsm.rb +43 -0
- data/lib/netutils/macaddr.rb +51 -0
- data/lib/netutils/oncequeue.rb +78 -0
- data/lib/netutils/parser.rb +30 -0
- data/lib/netutils/rib.rb +80 -0
- data/lib/netutils/switch.rb +402 -0
- data/lib/netutils/tunnel.rb +8 -0
- data/lib/netutils/version.rb +3 -0
- data/lib/netutils/vrf.rb +42 -0
- data/log/.gitignore +1 -0
- data/netutils.gemspec +33 -0
- metadata +195 -0
data/bin/mac-drop
ADDED
@@ -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
|
data/bin/mac-nodrop
ADDED
@@ -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
|
data/bin/port-shutdown
ADDED
@@ -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
|
data/bin/setup
ADDED
data/lib/netutils.rb
ADDED
@@ -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
|
data/lib/netutils/arp.rb
ADDED
@@ -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
|
data/lib/netutils/cli.rb
ADDED
@@ -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
|