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.
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotwired
4
+ class DB
5
+ class Device < Sequel::Model
6
+ end
7
+ end
8
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotwired
4
+ VERSION = "0.0.1"
5
+ end
data/lib/hotwired.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotwired
4
+ class HotwiredError < StandardError; end
5
+
6
+ require_relative "hotwired/core"
7
+ end
data/sig/hotwired.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Hotwired
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end