hotwired 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +306 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/hotwired +11 -0
- data/bin/setup +8 -0
- data/hotwired.gemspec +34 -0
- data/lib/hotwired/cli.rb +92 -0
- data/lib/hotwired/config.rb +33 -0
- data/lib/hotwired/core.rb +237 -0
- data/lib/hotwired/db/db.rb +61 -0
- data/lib/hotwired/db/model.rb +8 -0
- data/lib/hotwired/log.rb +18 -0
- data/lib/hotwired/model.rb +103 -0
- data/lib/hotwired/snmp.rb +119 -0
- data/lib/hotwired/version.rb +5 -0
- data/lib/hotwired.rb +7 -0
- data/sig/hotwired.rbs +4 -0
- metadata +141 -0
data/lib/hotwired/cli.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
class CLI
|
5
|
+
require "slop"
|
6
|
+
require_relative "../hotwired"
|
7
|
+
|
8
|
+
class NoConfig < HotwiredError; end
|
9
|
+
|
10
|
+
MAX_DELETE = 1
|
11
|
+
|
12
|
+
# 脚本调度函数入口
|
13
|
+
def run
|
14
|
+
# 检查 CLI 是否携带相关参数
|
15
|
+
if @opts[:poll]
|
16
|
+
Log.debug "Start hotwired with specify #{@opts[:poll]}"
|
17
|
+
Hotwired.new(cidr: @opts[:poll]).run
|
18
|
+
elsif @opts[:remove]
|
19
|
+
Log.debug "Start hotwired remove job"
|
20
|
+
remove_records @opts[:remove]
|
21
|
+
elsif @opts["purge-old"]
|
22
|
+
Log.debug "Start hotwired purge-old job"
|
23
|
+
remove_old @opts["purge-old"]
|
24
|
+
else
|
25
|
+
# 缺省的执行方式
|
26
|
+
Log.debug "Start hotwired with default params!"
|
27
|
+
Hotwired.new.run
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# 类对象实例化入口函数
|
32
|
+
def initialize
|
33
|
+
args, @opts = opts_parse
|
34
|
+
@arg = args.shift
|
35
|
+
CFG.debug = true if @opts[:debug]
|
36
|
+
raise NoConfig, "edit ~/.config/hotwired/config" if CONFIG.create
|
37
|
+
end
|
38
|
+
|
39
|
+
# 解析命令行脚本接收参数
|
40
|
+
def opts_parse
|
41
|
+
opts = Slop.parse do |o|
|
42
|
+
# banner "Usage: hotwired [options] [argument]"
|
43
|
+
o.on "-h", "--help", "show usage" do
|
44
|
+
puts o
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
o.bool "-d", "--debug", "Debugging on"
|
48
|
+
o.string "-p", "--poll", "Poll CIDR [argument]"
|
49
|
+
o.bool "-r", "--remove", "Remove [argument] from DB"
|
50
|
+
o.string "-m", "--max-delete", "Maximum number to delete, default #{MAX_DELETE}"
|
51
|
+
o.bool "-o", "--purge-old", "Remove records order than [argument] days"
|
52
|
+
o.bool "-s", "--simulate", "Simulate, do not change DB"
|
53
|
+
end
|
54
|
+
[opts.arguments, opts]
|
55
|
+
end
|
56
|
+
|
57
|
+
# 删除表记录
|
58
|
+
def remove_records(name)
|
59
|
+
Log.warn "Remove record #{name}"
|
60
|
+
DB.new
|
61
|
+
delete_records DB::Device.filter(Sequel.like(:ptr, "%#{name}%")).all
|
62
|
+
end
|
63
|
+
|
64
|
+
# 删除历史数据 【 N*天之前 】
|
65
|
+
def remove_old(days)
|
66
|
+
Log.warn "Remove #{days} days ago data"
|
67
|
+
old = (Time.now.utc - days.to_i * 24 * 60 * 60)
|
68
|
+
DB.new
|
69
|
+
delete_records DB::Device.filter { :last_seen < old }.all
|
70
|
+
end
|
71
|
+
|
72
|
+
# 删除某些主机数据:接收数组对象
|
73
|
+
def delete_records(devs)
|
74
|
+
Log.debug "delete_records #{devs.size}"
|
75
|
+
max_del = @opts["max-delete"] ? @opts["max-delete"] : MAX_DELETE
|
76
|
+
# 判断当前表已有数据条目和期望删除条目是否匹配
|
77
|
+
if devs.size > max_del.to_i
|
78
|
+
puts "Too many matching devices:"
|
79
|
+
devs.each do |dev|
|
80
|
+
puts " %s (%s)" % [dev.ptr, dev.ip]
|
81
|
+
end
|
82
|
+
puts "Be more specific"
|
83
|
+
else
|
84
|
+
puts "Deleting records:"
|
85
|
+
devs.each do |dev|
|
86
|
+
puts " %s (%s)" % [dev.ptr, dev.ip]
|
87
|
+
dev.delete unless @opts[:simulate]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
require "strada"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
class Config
|
8
|
+
ROOT = File.join ENV["HOME"], ".config", "hotwired"
|
9
|
+
CRASH = File.join ROOT, "crash"
|
10
|
+
end
|
11
|
+
|
12
|
+
FileUtils.mkdir_p Config::ROOT
|
13
|
+
CONFIG = Strada.new name: "hotwired", load: "false", key_to_s: true
|
14
|
+
|
15
|
+
CONFIG.default.community = "cisco"
|
16
|
+
CONFIG.default.db = File.join Config::ROOT, "hotwired.db"
|
17
|
+
CONFIG.default.poll = %w( 192.168.8.0/24 )
|
18
|
+
CONFIG.default.ignore = %w( 192.168.8.100 )
|
19
|
+
CONFIG.default.mgmt = %w( lo0.0 loopback0 vlan2 )
|
20
|
+
CONFIG.default.threads = 50
|
21
|
+
CONFIG.default.timeout = 0.25
|
22
|
+
CONFIG.default.retries = 2
|
23
|
+
CONFIG.default.log = File.join Config::ROOT, "log"
|
24
|
+
CONFIG.default.debug = true
|
25
|
+
|
26
|
+
# 加载初始化配置
|
27
|
+
CONFIG.load
|
28
|
+
CFG = CONFIG.cfg
|
29
|
+
|
30
|
+
# 初始化项目日志参数
|
31
|
+
Log.file = CFG.log if CFG.log
|
32
|
+
Log.level = CFG.debug ? Logger::INFO : Logger::DEBUG
|
33
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# 加载项目依赖
|
4
|
+
require_relative "log"
|
5
|
+
require_relative "config"
|
6
|
+
require_relative "snmp"
|
7
|
+
require_relative "db/db"
|
8
|
+
require_relative "model"
|
9
|
+
|
10
|
+
# 加载外部依赖
|
11
|
+
require "ipaddr"
|
12
|
+
require "resolv"
|
13
|
+
|
14
|
+
module Hotwired
|
15
|
+
# 类方法属性
|
16
|
+
class << self
|
17
|
+
# 实例化 Core 对象
|
18
|
+
def new(opts = {})
|
19
|
+
Core.new opts
|
20
|
+
end
|
21
|
+
|
22
|
+
# 轮询 host 数据
|
23
|
+
def poll(opts = {})
|
24
|
+
host = opts.delete :host
|
25
|
+
raise HotwiredError, "'host' not given" unless host
|
26
|
+
hotwire = new(opts)
|
27
|
+
result = hotwire.poll Resolv.getaddress(host)
|
28
|
+
# 数据转储
|
29
|
+
hotwire.make_record result if result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# 类对象方法属性
|
34
|
+
class Core
|
35
|
+
# 类对象初始化函数入口
|
36
|
+
def initialize(opts = {})
|
37
|
+
@opts = opts
|
38
|
+
@community = opts.delete(:community) || CFG.community
|
39
|
+
end
|
40
|
+
|
41
|
+
# 类对象外部调用函数入口
|
42
|
+
def run
|
43
|
+
# 解析变量
|
44
|
+
cidr = @opts.delete :cidr
|
45
|
+
# @output = @opts.delete :output
|
46
|
+
|
47
|
+
# 设置缺省 logger 输出
|
48
|
+
# unless @output
|
49
|
+
# @output = Logger.new $stdout
|
50
|
+
# @output.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
51
|
+
# end
|
52
|
+
|
53
|
+
# 初始化变量及遍历 CIDR
|
54
|
+
poll, ignores = resolve_networks cidr
|
55
|
+
# 实例化线程和数据库联结
|
56
|
+
@mutex = Mutex.new
|
57
|
+
@db = DB.new
|
58
|
+
threads = []
|
59
|
+
# 线程遇到异常及时终止
|
60
|
+
Thread.abort_on_exception = true
|
61
|
+
|
62
|
+
# 遍历待轮询的 IPAddr
|
63
|
+
poll.each do |net|
|
64
|
+
net.to_range.each do |ip|
|
65
|
+
# 检查当前地址是否需要被忽略
|
66
|
+
next if ignores.any? { |ignore| ignore.include? ip }
|
67
|
+
# 清除空闲线程
|
68
|
+
while threads.size >= CFG.threads
|
69
|
+
threads.delete_if { |thread| not thread.alive? }
|
70
|
+
sleep 0.01
|
71
|
+
end
|
72
|
+
# 线程不够则主动添加线程
|
73
|
+
threads << Thread.new do
|
74
|
+
result = poll ip
|
75
|
+
@mutex.synchronize { process result } if result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# 激活线程,开始干活
|
80
|
+
threads.each { |thread| thread.join }
|
81
|
+
end
|
82
|
+
|
83
|
+
# 轮询单个 IP 设备信息
|
84
|
+
def poll(ip)
|
85
|
+
result = nil
|
86
|
+
# 实例化 SNMP 对象,批量获取相关监控数据
|
87
|
+
snmp = SNMP.new(ip.to_s, @community)
|
88
|
+
oids = snmp.dbget
|
89
|
+
|
90
|
+
if oids
|
91
|
+
# 早期异常拦截
|
92
|
+
Log.debug "SNMP::NoSuchObject #{ip}" if oids[:sysDescr] == ::SNMP::NoSuchObject
|
93
|
+
return nil if oids[:sysDescr] == ::SNMP::NoSuchObject
|
94
|
+
# 初始化变量,并尝试刷新接口描述信息
|
95
|
+
result = { oids: oids, ip: ip, int: "n/a" }
|
96
|
+
# 联机查询数据
|
97
|
+
index = snmp.ip2index(ip.to_s)
|
98
|
+
int = snmp.ifdescr(index)
|
99
|
+
# 逻辑处理
|
100
|
+
if index
|
101
|
+
if int
|
102
|
+
result[:int] = int.downcase
|
103
|
+
else
|
104
|
+
Log.debug "no ifDescr for #{index} at #{ip}"
|
105
|
+
end
|
106
|
+
else
|
107
|
+
Log.debug "no ifIndex for #{ip}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# 关闭 SNMP 会话并返回结果
|
111
|
+
snmp.close
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
115
|
+
# 新增表记录
|
116
|
+
def make_record(opt)
|
117
|
+
{
|
118
|
+
ip: opt[:ip].to_s,
|
119
|
+
ptr: ip2name(opt[:ip].to_s),
|
120
|
+
model: Model.map(opt[:oids][:sysDescr], opt[:oids][:sysObjectID]),
|
121
|
+
oid_ifDescr: opt[:int],
|
122
|
+
oid_sysName: opt[:oids][:sysName],
|
123
|
+
oid_sysLocation: opt[:oids][:sysLocation],
|
124
|
+
oid_sysDescr: opt[:oids][:sysDescr],
|
125
|
+
oid_sysObjectID: opt[:oids][:sysObjectID].join("."),
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
def process(opt)
|
131
|
+
opt = normalize_opt opt
|
132
|
+
record = make_record opt
|
133
|
+
# 查询表中已有数据
|
134
|
+
old_by_ip, old_by_sysname = @db.old(record[:ip], record[:oid_sysName])
|
135
|
+
|
136
|
+
# unique box having non-unique sysname
|
137
|
+
# old_by_sysname = false if record[:oid_sysDescr].match 'Application Control Engine'
|
138
|
+
|
139
|
+
# 查无记录则需要新增
|
140
|
+
if (not old_by_sysname) && (not old_by_ip)
|
141
|
+
# all new device
|
142
|
+
@output.info "ptr [%s] sysName [%s] ip [%s]" % [record[:ptr], record[:oid_sysName], record[:ip]]
|
143
|
+
Log.info "#{record[:ip]} added"
|
144
|
+
@db.add record
|
145
|
+
# 根据 IP 可以查询到数据,但设备名称发生变化
|
146
|
+
elsif (not old_by_sysname) && old_by_ip
|
147
|
+
# IP seen, name not, device got renamed?
|
148
|
+
Log.info "#{record[:ip]} got renamed"
|
149
|
+
@db.update record, [:ip, old_by_ip[:ip]]
|
150
|
+
# 根据设备名称可以查询到数据,但 IP 地址发生变化
|
151
|
+
elsif old_by_sysname && (not old_by_ip)
|
152
|
+
# name exists, but IP is new, figure out if we wan to use old or new IP
|
153
|
+
decide_old_new record, old_by_sysname
|
154
|
+
# 已有记录刷新即可
|
155
|
+
elsif old_by_sysname && old_by_ip
|
156
|
+
both_seen record, old_by_sysname, old_by_ip
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# 根据 IP 和设备名称均可检索到数据,需要进一步判断查询出来的记录是否完全一致
|
161
|
+
def both_seen(record, old_by_sysname, old_by_ip)
|
162
|
+
if old_by_sysname == old_by_ip
|
163
|
+
# no changes, updating same record
|
164
|
+
Log.debug "#{record[:ip]} refreshed, no channges"
|
165
|
+
@db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
|
166
|
+
else
|
167
|
+
# same name seen and same IP seen, but records were not same (device got renumbered to existing node + existing node got delete?)
|
168
|
+
Log.warn "#{record[:ip]}, unique entries for IP and sysName in DB, updating by IP"
|
169
|
+
@db.update record, [:ip, old_by_ip[:ip]]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# 设备新旧记录优先级校验
|
174
|
+
# 新数据比旧数据优先级更高,可以理解更新可信?
|
175
|
+
def decide_old_new(record, old_by_sysname)
|
176
|
+
new_int_pref = (CFG.mgmt.index(record[:oid_ifDescr]) or 100)
|
177
|
+
old_int_pref = (CFG.mgmt.index(old_by_sysname[:oid_ifDescr]) or 99)
|
178
|
+
|
179
|
+
if new_int_pref < old_int_pref
|
180
|
+
# 原有数据优先级更高
|
181
|
+
# new int is more preferable than old
|
182
|
+
Log.info "#{record[:ip]} is replacing inferior #{old_by_sysname[:ip]}"
|
183
|
+
@db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
|
184
|
+
elsif (new_int_pref == 100) && (old_int_pref == 99)
|
185
|
+
# 新老数据接口均未标注为管理口
|
186
|
+
# neither old or new interface is known good MGMT interface
|
187
|
+
if SNMP.new(old_by_sysname[:ip], @community).sysdescr
|
188
|
+
# 如果老IP可以检索数据,则无需更新
|
189
|
+
# if old IP works, don't update
|
190
|
+
Log.debug "#{record[:ip]} not updating, previously seen as #{old_by_sysname[:ip]}"
|
191
|
+
else
|
192
|
+
Log.info "#{record[:ip]} updating, old #{old_by_sysname[:ip]} is dead"
|
193
|
+
@db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
|
194
|
+
end
|
195
|
+
elsif new_int_pref >= old_int_pref
|
196
|
+
# 新数据优先级高于老数据,则无需进一步处理
|
197
|
+
# nothing to do, we have better entry
|
198
|
+
Log.debug "#{record[:ip]} already seen as superior via #{old_by_sysname[:ip]}"
|
199
|
+
else
|
200
|
+
Log.error "not updating, new: #{record[:ip]}, old: #{old_by_sysname[:ip]}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# 序列化 opt 参数
|
205
|
+
def normalize_opt(opt)
|
206
|
+
opt[:oids][:sysName].sub!(/-re[1-9]\./, "-re0.")
|
207
|
+
opt
|
208
|
+
end
|
209
|
+
|
210
|
+
# 解析 IP 关联的主机名
|
211
|
+
def ip2name(ip)
|
212
|
+
Resolv.getname ip rescue ip
|
213
|
+
end
|
214
|
+
|
215
|
+
# 解析 cidr
|
216
|
+
def resolve_networks(cidr)
|
217
|
+
# 如未接收外部变量则使用缺省值
|
218
|
+
cidr = cidr ? [cidr].flatten : CFG.poll
|
219
|
+
|
220
|
+
# 从 CIDR 中剔除排除清单
|
221
|
+
# 支持数组以及文本形式,数据返回包含2个数组对象的数组
|
222
|
+
[cidr, CFG.ignore].map do |nets|
|
223
|
+
if nets.respond_to? :each
|
224
|
+
nets.map { |net| IPAddr.new net }
|
225
|
+
else
|
226
|
+
out = []
|
227
|
+
File.read(nets).each_line do |net|
|
228
|
+
# 模糊的 IP 地址正则表达式
|
229
|
+
net = net.match(/^([\d.\/]+)$/)
|
230
|
+
out << IPAddr.new(net[1]) if net
|
231
|
+
end
|
232
|
+
out
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
class DB
|
5
|
+
require "sequel"
|
6
|
+
require "sqlite3"
|
7
|
+
|
8
|
+
# 类对象初始化函数入口
|
9
|
+
def initialize
|
10
|
+
Log.debug "Initialize Hotwired object ... "
|
11
|
+
@db = Sequel.sqlite(CFG.db, max_connections: 1, pool_timeout: 60)
|
12
|
+
create_table # unless @db.table_exists?(:devices)
|
13
|
+
require_relative "model"
|
14
|
+
end
|
15
|
+
|
16
|
+
# 新建数据库记录
|
17
|
+
def add(record)
|
18
|
+
# 添加时间戳属性
|
19
|
+
record[:first_seen] = record[:last_seen] = Time.now.utc
|
20
|
+
record[:active] = true
|
21
|
+
Log.debug "adding: #{record}"
|
22
|
+
Device.new(record).save
|
23
|
+
end
|
24
|
+
|
25
|
+
# 更新数据
|
26
|
+
def update(record, where)
|
27
|
+
# 更新数据入库时间
|
28
|
+
record[:last_seen] = Time.now.utc
|
29
|
+
record[:active] = true
|
30
|
+
Log.debug "updating (where: #{where}): #{record}"
|
31
|
+
Device[where.first.to_sym => where.last].update(record)
|
32
|
+
end
|
33
|
+
|
34
|
+
# 查询数据表记录信息
|
35
|
+
def old(ip, oid_sysname)
|
36
|
+
ip = Device[ip: ip]
|
37
|
+
sysname = Device[oid_sysName: oid_sysname]
|
38
|
+
[ip, sysname]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def create_table
|
43
|
+
# 检查是否存在数据表结构,不存在则新建
|
44
|
+
@db.create_table? :devices do
|
45
|
+
primary_key :id
|
46
|
+
String :ip
|
47
|
+
String :ptr
|
48
|
+
String :model
|
49
|
+
String :oid_ifDescr
|
50
|
+
Boolean :active
|
51
|
+
Time :first_seen
|
52
|
+
Time :last_seen
|
53
|
+
String :oid_sysName
|
54
|
+
String :oid_sysLocation
|
55
|
+
String :oid_sysDescr
|
56
|
+
String :oid_sysObjectID
|
57
|
+
end
|
58
|
+
Log.debug "Creating SQLITE DB"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/hotwired/log.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
class Logger < Logger
|
7
|
+
def initialize(target = STDOUT)
|
8
|
+
super target
|
9
|
+
self.level = Logger::DEBUG
|
10
|
+
end
|
11
|
+
|
12
|
+
def file=(target)
|
13
|
+
@log_device = LogDevice.new target
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Log = Logger.new
|
18
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
class Model
|
5
|
+
def self.map(sysDescr, sysObjectID)
|
6
|
+
case sysDescr
|
7
|
+
when /Cisco Catalyst Operating System/i
|
8
|
+
"catos"
|
9
|
+
when /Cisco Controller/
|
10
|
+
"aireos"
|
11
|
+
when /IOS XR/
|
12
|
+
"iosxr"
|
13
|
+
when /NX-OS/
|
14
|
+
"nxos"
|
15
|
+
when /JUNOS/
|
16
|
+
"junos"
|
17
|
+
when /Arista Networks EOS/
|
18
|
+
"eos"
|
19
|
+
when /IronWare/
|
20
|
+
"ironware"
|
21
|
+
when /TiMOS/
|
22
|
+
"timos"
|
23
|
+
when /ExtremeXOS/
|
24
|
+
"xos"
|
25
|
+
when /Cisco Adaptive Security Appliance/
|
26
|
+
"asa"
|
27
|
+
when /Brocade Fibre Channel Switch/
|
28
|
+
"fabricos"
|
29
|
+
when /Brocade VDX/
|
30
|
+
"nos"
|
31
|
+
when /cisco/i, /Application Control Engine/i
|
32
|
+
"ios"
|
33
|
+
when /Force10 OS/
|
34
|
+
"ftos"
|
35
|
+
when /Versatile Routing Platform/
|
36
|
+
"vrp"
|
37
|
+
when /^NetScreen/, /^SSG-\d+/
|
38
|
+
"screenos"
|
39
|
+
when /^Summit/
|
40
|
+
"xos"
|
41
|
+
when /^Alcatel-Lucent \S+ [789]\./ # aos <7 is vxworks, >=7 is linux
|
42
|
+
"aos7"
|
43
|
+
when /^AOS-W/
|
44
|
+
"aosw"
|
45
|
+
when /^Alcatel-Lucent/
|
46
|
+
"aos"
|
47
|
+
when /\s+ACOS\s+/
|
48
|
+
"acos"
|
49
|
+
when /ProCurve/ # ProCurve OS does not seem to have name?
|
50
|
+
"procurve"
|
51
|
+
when /ASAM/
|
52
|
+
"isam"
|
53
|
+
when /^\d+[A-Z]\sEthernet Switch$/
|
54
|
+
"powerconnect"
|
55
|
+
when /Ericsson IPOS/
|
56
|
+
"ssr"
|
57
|
+
when /Huawei Integrated Access Software/
|
58
|
+
"hias"
|
59
|
+
else
|
60
|
+
case sysObjectID
|
61
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.12356."))
|
62
|
+
"fortios" # 1.3.6.1.4.1.12356.101.1.10004
|
63
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.6486."))
|
64
|
+
"aos" # 1.3.6.1.4.1.6486.800.1.1.2.1.11.2.2
|
65
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.6027."))
|
66
|
+
"ftos" # 1.3.6.1.4.1.6027.1.3.4
|
67
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.1588."))
|
68
|
+
"fabricos " # 1.3.6.1.4.1.1588.2.1.1.1
|
69
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.3224."))
|
70
|
+
"screenos" # 1.3.6.1.4.1.3224.1.51 (SSG) 1.16 (Netscreen 2k)
|
71
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.674."))
|
72
|
+
"powerconnect" # 1.3.6.1.4.1.674.10895.3031
|
73
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.22610."))
|
74
|
+
"acos" # 1.3.6.1.4.1.22610.1.3.14
|
75
|
+
when Regexp.new("^" + Regexp.quote(".1.3.6.1.4.1.637."))
|
76
|
+
"isam" # 1.3.6.1.4.1.637.61.1
|
77
|
+
when Regexp.new("^" + Regexp.quote(".1.3.6.1.4.1.2011."))
|
78
|
+
"vrp" # 1.3.6.1.4.1.2011.2.224.67 (AR1220F)
|
79
|
+
when Regexp.new("^" + Regexp.quote(".1.3.6.1.4.1.1588."))
|
80
|
+
"nos" # 1.3.6.1.4.1.1588.2.2.1.1.1.5 (VDX)
|
81
|
+
when Regexp.new("^" + Regexp.quote(".1.3.6.1.4.1.1916."))
|
82
|
+
"xos" # 1.3.6.1.4.1.1916.2.76 (X450a-48t)
|
83
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.9.1.745"))
|
84
|
+
"asa" # 1.3.6.1.4.1.9.1.745
|
85
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.20858.2.600"))
|
86
|
+
"casa" # 1.3.6.1.4.1.20858.2.600
|
87
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.2011.2.169"))
|
88
|
+
"hias" # 1.3.6.1.4.1.2011.2.169
|
89
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.2011.2.300"))
|
90
|
+
"hias" # 1.3.6.1.4.1.2011.2.300 (MA5800 OLT)
|
91
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.2352.1"))
|
92
|
+
"ssr" # 1.3.6.1.4.1.2352.1.17 and .18
|
93
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.193.218.1"))
|
94
|
+
"ssr" # 1.3.6.1.4.1.193.218.1.17 and .18
|
95
|
+
when Regexp.new("^" + Regexp.quote("1.3.6.1.4.1.11.2.3.7.11"))
|
96
|
+
"procurve" # Aruba switches are really HP Procurve
|
97
|
+
else
|
98
|
+
"unsupported"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hotwired
|
4
|
+
class SNMP
|
5
|
+
require "snmp"
|
6
|
+
|
7
|
+
class InvalidResponse < StandardError; end
|
8
|
+
|
9
|
+
# 需要遍历 oid 写入数据库的相关属性
|
10
|
+
DB_OID = {
|
11
|
+
sysDescr: "1.3.6.1.2.1.1.1.0", # 运行系统描述
|
12
|
+
sysObjectID: "1.3.6.1.2.1.1.2.0", # 厂商识别
|
13
|
+
sysName: "1.3.6.1.2.1.1.5.0", # 系统名称
|
14
|
+
sysLocation: "1.3.6.1.2.1.1.6.0", # 系统位置
|
15
|
+
}
|
16
|
+
|
17
|
+
OID = {
|
18
|
+
ifDescr: "1.3.6.1.2.1.2.2.1.2", # 接口描述
|
19
|
+
ipCidrRouteIfIndex: "1.3.6.1.2.1.4.24.4.1.5", # addr.255.255.255.255.0.0.0.0.0
|
20
|
+
ipAdEntIfIndex: "1.3.6.1.2.1.4.20.1.2", # addr | 旧格式
|
21
|
+
ipAddressIfIndex: "1.3.6.1.2.1.4.34.1.3", # 1,2 (uni,any) . 4,16 (size) . addr | 新格式
|
22
|
+
}
|
23
|
+
UNICAST = 1
|
24
|
+
IPV4 = 4
|
25
|
+
BULK_MAX = 30
|
26
|
+
|
27
|
+
# 类对象初始化函数入口
|
28
|
+
def initialize(host, community = CFG.community)
|
29
|
+
@snmp = ::SNMP::Manager.new(Host: host, Community: community, Timeout: CFG.timeout, Retries: CFG.retries, MibModules: [])
|
30
|
+
end
|
31
|
+
|
32
|
+
# 关闭会话
|
33
|
+
def close
|
34
|
+
@snmp.close
|
35
|
+
end
|
36
|
+
|
37
|
+
# 查询 oid 监控数据
|
38
|
+
# 支持多个片段拼接成一个完整的 OID 字串
|
39
|
+
def get(*oid)
|
40
|
+
oid = [oid].flatten.join(".")
|
41
|
+
begin
|
42
|
+
@snmp.get(oid).each_varbind { |vb| return vb }
|
43
|
+
rescue ::SNMP::RequestTimeout, Errno::EACCES
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# 批量查询 oid,入参为 HASH 数据结构
|
49
|
+
def mget(oids = DB_OID)
|
50
|
+
result = {}
|
51
|
+
begin
|
52
|
+
res = @snmp.get(oids.map { |_, oid| oid })
|
53
|
+
# 抛出异常
|
54
|
+
raise InvalidResponse, "#{res.error_status} from #{@snmp.config[:host]}" unless res.error_status == :noError
|
55
|
+
res.each_varbind do |vb|
|
56
|
+
oids.each do |key, oid|
|
57
|
+
if vb.name.to_str == oid
|
58
|
+
result[key] = vb.value
|
59
|
+
# 找到即跳出当前循环体
|
60
|
+
next
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue ::SNMP::RequestTimeout, Errno::EACCES
|
65
|
+
return false
|
66
|
+
rescue InvalidResponse
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
# 方法别名
|
73
|
+
alias dbget mget
|
74
|
+
|
75
|
+
# 遍历根节点检索数据
|
76
|
+
def bulkwalk(root)
|
77
|
+
# 初始化遍历
|
78
|
+
last, oid, vbs = false, root, []
|
79
|
+
# 轮询 root oid 主体逻辑
|
80
|
+
until last
|
81
|
+
r = @snmp.get_bulk 0, BULK_MAX, oid
|
82
|
+
r.varbind_list.each do |vb|
|
83
|
+
oid = vb.name.to_str
|
84
|
+
# 跳出循环条件
|
85
|
+
(last = true; break) unless oid.match?(/^#{Regexp.quote root}/)
|
86
|
+
vbs << vb
|
87
|
+
end
|
88
|
+
end
|
89
|
+
vbs
|
90
|
+
end
|
91
|
+
|
92
|
+
# 查询设备描述
|
93
|
+
def sysdescr
|
94
|
+
get DB_OID[:sysDescr]
|
95
|
+
end
|
96
|
+
|
97
|
+
# IP 转索引
|
98
|
+
def ip2index(ip)
|
99
|
+
# 根据 IP 查询其路由及接口IP索引序号
|
100
|
+
oids = mget(route: [OID[:ipCidrRouteIfIndex], ip, "255.255.255.255.0.0.0.0.0"].join("."),
|
101
|
+
new: [OID[:ipAddressIfIndex], UNICAST, IPV4, ip].join("."),
|
102
|
+
old: [OID[:ipAdEntIfIndex], ip].join("."))
|
103
|
+
return false unless oids
|
104
|
+
# 优先使用 route 命中,如查询不到则进一步尝试使用其他属性
|
105
|
+
index = oids[:route]
|
106
|
+
index = oids[:new] if (not index.class == ::SNMP::Integer) || (index.to_s == "0")
|
107
|
+
index = oids[:old] if (not index.class == ::SNMP::Integer) || (index.to_s == "0")
|
108
|
+
return false unless index.class == ::SNMP::Integer
|
109
|
+
index.to_s
|
110
|
+
end
|
111
|
+
|
112
|
+
# 查询接口描述
|
113
|
+
def ifdescr(index)
|
114
|
+
descr = get OID[:ifDescr], index
|
115
|
+
return false unless descr && (descr.value.class == ::SNMP::OctetString)
|
116
|
+
descr.value.to_s
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/hotwired.rb
ADDED
data/sig/hotwired.rbs
ADDED