hotwired 0.0.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.
- 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