# frozen_string_literal: true # 加载项目依赖 require_relative "log" require_relative "config" require_relative "snmp" require_relative "db/db" require_relative "model" # 加载外部依赖 require "ipaddr" require "resolv" module Hotwired # 类方法属性 class << self # 实例化 Core 对象 def new(opts = {}) Core.new opts end # 轮询 host 数据 def poll(opts = {}) host = opts.delete :host raise HotwiredError, "'host' not given" unless host hotwire = new(opts) result = hotwire.poll Resolv.getaddress(host) # 数据转储 hotwire.make_record result if result end end # 类对象方法属性 class Core # 类对象初始化函数入口 def initialize(opts = {}) @opts = opts @community = opts.delete(:community) || CFG.community end # 类对象外部调用函数入口 def run # 解析变量 cidr = @opts.delete :cidr # @output = @opts.delete :output # 设置缺省 logger 输出 # unless @output # @output = Logger.new $stdout # @output.formatter = proc { |_, _, _, msg| "#{msg}\n" } # end # 初始化变量及遍历 CIDR poll, ignores = resolve_networks cidr # 实例化线程和数据库联结 @mutex = Mutex.new @db = DB.new threads = [] # 线程遇到异常及时终止 Thread.abort_on_exception = true # 遍历待轮询的 IPAddr poll.each do |net| net.to_range.each do |ip| # 检查当前地址是否需要被忽略 next if ignores.any? { |ignore| ignore.include? ip } # 清除空闲线程 while threads.size >= CFG.threads threads.delete_if { |thread| not thread.alive? } sleep 0.01 end # 线程不够则主动添加线程 threads << Thread.new do result = poll ip @mutex.synchronize { process result } if result end end end # 激活线程,开始干活 threads.each { |thread| thread.join } end # 轮询单个 IP 设备信息 def poll(ip) result = nil # 实例化 SNMP 对象,批量获取相关监控数据 snmp = SNMP.new(ip.to_s, @community) oids = snmp.dbget if oids # 早期异常拦截 Log.debug "SNMP::NoSuchObject #{ip}" if oids[:sysDescr] == ::SNMP::NoSuchObject return nil if oids[:sysDescr] == ::SNMP::NoSuchObject # 初始化变量,并尝试刷新接口描述信息 result = { oids: oids, ip: ip, int: "n/a" } # 联机查询数据 index = snmp.ip2index(ip.to_s) int = snmp.ifdescr(index) # 逻辑处理 if index if int result[:int] = int.downcase else Log.debug "no ifDescr for #{index} at #{ip}" end else Log.debug "no ifIndex for #{ip}" end end # 关闭 SNMP 会话并返回结果 snmp.close result end # 新增表记录 def make_record(opt) { ip: opt[:ip].to_s, ptr: ip2name(opt[:ip].to_s), model: Model.map(opt[:oids][:sysDescr], opt[:oids][:sysObjectID]), oid_ifDescr: opt[:int], oid_sysName: opt[:oids][:sysName], oid_sysLocation: opt[:oids][:sysLocation], oid_sysDescr: opt[:oids][:sysDescr], oid_sysObjectID: opt[:oids][:sysObjectID].join("."), } end private def process(opt) opt = normalize_opt opt record = make_record opt # 查询表中已有数据 old_by_ip, old_by_sysname = @db.old(record[:ip], record[:oid_sysName]) # unique box having non-unique sysname # old_by_sysname = false if record[:oid_sysDescr].match 'Application Control Engine' # 查无记录则需要新增 if (not old_by_sysname) && (not old_by_ip) # all new device @output.info "ptr [%s] sysName [%s] ip [%s]" % [record[:ptr], record[:oid_sysName], record[:ip]] Log.info "#{record[:ip]} added" @db.add record # 根据 IP 可以查询到数据,但设备名称发生变化 elsif (not old_by_sysname) && old_by_ip # IP seen, name not, device got renamed? Log.info "#{record[:ip]} got renamed" @db.update record, [:ip, old_by_ip[:ip]] # 根据设备名称可以查询到数据,但 IP 地址发生变化 elsif old_by_sysname && (not old_by_ip) # name exists, but IP is new, figure out if we wan to use old or new IP decide_old_new record, old_by_sysname # 已有记录刷新即可 elsif old_by_sysname && old_by_ip both_seen record, old_by_sysname, old_by_ip end end # 根据 IP 和设备名称均可检索到数据,需要进一步判断查询出来的记录是否完全一致 def both_seen(record, old_by_sysname, old_by_ip) if old_by_sysname == old_by_ip # no changes, updating same record Log.debug "#{record[:ip]} refreshed, no channges" @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]] else # same name seen and same IP seen, but records were not same (device got renumbered to existing node + existing node got delete?) Log.warn "#{record[:ip]}, unique entries for IP and sysName in DB, updating by IP" @db.update record, [:ip, old_by_ip[:ip]] end end # 设备新旧记录优先级校验 # 新数据比旧数据优先级更高,可以理解更新可信? def decide_old_new(record, old_by_sysname) new_int_pref = (CFG.mgmt.index(record[:oid_ifDescr]) or 100) old_int_pref = (CFG.mgmt.index(old_by_sysname[:oid_ifDescr]) or 99) if new_int_pref < old_int_pref # 原有数据优先级更高 # new int is more preferable than old Log.info "#{record[:ip]} is replacing inferior #{old_by_sysname[:ip]}" @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]] elsif (new_int_pref == 100) && (old_int_pref == 99) # 新老数据接口均未标注为管理口 # neither old or new interface is known good MGMT interface if SNMP.new(old_by_sysname[:ip], @community).sysdescr # 如果老IP可以检索数据,则无需更新 # if old IP works, don't update Log.debug "#{record[:ip]} not updating, previously seen as #{old_by_sysname[:ip]}" else Log.info "#{record[:ip]} updating, old #{old_by_sysname[:ip]} is dead" @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]] end elsif new_int_pref >= old_int_pref # 新数据优先级高于老数据,则无需进一步处理 # nothing to do, we have better entry Log.debug "#{record[:ip]} already seen as superior via #{old_by_sysname[:ip]}" else Log.error "not updating, new: #{record[:ip]}, old: #{old_by_sysname[:ip]}" end end # 序列化 opt 参数 def normalize_opt(opt) opt[:oids][:sysName].sub!(/-re[1-9]\./, "-re0.") opt end # 解析 IP 关联的主机名 def ip2name(ip) Resolv.getname ip rescue ip end # 解析 cidr def resolve_networks(cidr) # 如未接收外部变量则使用缺省值 cidr = cidr ? [cidr].flatten : CFG.poll # 从 CIDR 中剔除排除清单 # 支持数组以及文本形式,数据返回包含2个数组对象的数组 [cidr, CFG.ignore].map do |nets| if nets.respond_to? :each nets.map { |net| IPAddr.new net } else out = [] File.read(nets).each_line do |net| # 模糊的 IP 地址正则表达式 net = net.match(/^([\d.\/]+)$/) out << IPAddr.new(net[1]) if net end out end end end end end