Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/snmp/manager.rb
Views: 11766
#1# Copyright (c) 2004 David R. Halliday2# All rights reserved.3#4# This SNMP library is free software. Redistribution is permitted under the5# same terms and conditions as the standard Ruby distribution. See the6# COPYING file in the Ruby distribution for details.7#89require 'snmp/pdu'10require 'snmp/mib'11require 'socket'12require 'timeout'13require 'thread'1415module SNMP1617class RequestTimeout < RuntimeError; end1819##20# Wrap socket so that it can be easily substituted for testing or for21# using other transport types (e.g. TCP)22#23class UDPTransport24def initialize(socket = nil)25@socket = socket2627if socket.nil?28@socket = UDPSocket.open29end30end3132def close33@socket.close34end3536def send(data, host, port)37@socket.send(data, 0, host, port)38end3940def recv(max_bytes)41@socket.recv(max_bytes)42end43end444546class RexUDPTransport47def initialize(socket = nil)48@socket = socket4950if socket.nil?51@socket = UDPSocket.open52end53end5455def close56@socket.close57end5859def send(data, host, port, flags = 0)60begin61@socket.sendto(data, host, port, flags)62rescue NoMethodError63@socket.send(data, 0, host, port)64rescue ::Errno::EISCONN65@socket.write(data)66end67end6869def recv(max_bytes)70@socket.recv(max_bytes)71end72end737475##76# Manage a request-id in the range 1..2**31-177#78class RequestId79MAX_REQUEST_ID = 2**318081def initialize82@lock = Mutex.new83@request_id = rand(MAX_REQUEST_ID)84end8586def next87@lock.synchronize do88@request_id += 189@request_id = 1 if @request_id == MAX_REQUEST_ID90return @request_id91end92end9394def force_next(next_id)95new_request_id = next_id.to_i96if new_request_id < 1 || new_request_id >= MAX_REQUEST_ID97raise "Invalid request id: #{new_request_id}"98end99new_request_id = MAX_REQUEST_ID if new_request_id == 1100@lock.synchronize do101@request_id = new_request_id - 1102end103end104end105106##107# == SNMP Manager108#109# This class provides a manager for interacting with a single SNMP agent.110#111# = Example112#113# require 'snmp'114#115# manager = SNMP::Manager.new(:Host => 'localhost', :Port => 1061)116# response = manager.get(["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"])117# response.each_varbind {|vb| puts vb.inspect}118# manager.close119#120# == Symbolic Object Names121#122# Symbolic names for SNMP object IDs can be used as parameters to the123# APIs in this class if the MIB modules are imported and the names of the124# MIBs are included in the MibModules configuration parameter.125#126# See MIB.varbind_list for a description of valid parameter formats.127#128# The following modules are loaded by default: "SNMPv2-SMI", "SNMPv2-MIB",129# "IF-MIB", "IP-MIB", "TCP-MIB", "UDP-MIB". All of the current IETF MIBs130# have been imported and are available for loading.131#132# Additional modules may be imported using the MIB class. The133# current implementation of the importing code requires that the134# external 'smidump' tool is available in your PATH. This tool can be135# obtained from the libsmi website at136# http://www.ibr.cs.tu-bs.de/projects/libsmi/ .137#138# = Example139#140# Do this once:141#142# SNMP::MIB.import_module(MY_MODULE_FILENAME, MIB_OUTPUT_DIR)143#144# Include your module in MibModules each time you create a Manager:145#146# SNMP::Manager.new(:Host => 'localhost', :MibDir => MIB_OUTPUT_DIR,147# :MibModules => ["MY-MODULE-MIB", "SNMPv2-MIB", ...])148#149150class Manager151152##153# Default configuration. Individual options may be overridden when154# the Manager is created.155#156DefaultConfig = {157:Host => 'localhost',158:Port => 161,159:TrapPort => 162,160:Socket => nil,161:Community => 'public',162:WriteCommunity => nil,163:Version => :SNMPv2c,164:Timeout => 1,165:Retries => 5,166:Transport => UDPTransport,167:MaxReceiveBytes => 8000,168:MibDir => MIB::DEFAULT_MIB_PATH,169:MibModules => ["SNMPv2-SMI", "SNMPv2-MIB", "IF-MIB", "IP-MIB", "TCP-MIB", "UDP-MIB"]}170171@@request_id = RequestId.new172173##174# Retrieves the current configuration of this Manager.175#176attr_reader :config177178##179# Retrieves the MIB for this Manager.180#181attr_reader :mib182183def initialize(config = {})184if block_given?185warn "SNMP::Manager::new() does not take block; use SNMP::Manager::open() instead"186end187@config = DefaultConfig.merge(config)188@config[:WriteCommunity] = @config[:WriteCommunity] || @config[:Community]189@host = @config[:Host]190@port = @config[:Port]191@socket = @config[:Socket]192@trap_port = @config[:TrapPort]193@community = @config[:Community]194@write_community = @config[:WriteCommunity]195@snmp_version = @config[:Version]196@timeout = @config[:Timeout]197@retries = @config[:Retries]198@transport = @config[:Transport].new(@socket)199@max_bytes = @config[:MaxReceiveBytes]200@mib = MIB.new201load_modules(@config[:MibModules], @config[:MibDir])202end203204##205# Creates a Manager but also takes an optional block and automatically206# closes the transport connection used by this manager after the block207# completes.208#209def self.open(config = {})210manager = Manager.new(config)211if block_given?212begin213yield manager214ensure215manager.close216end217end218end219220##221# Close the transport connection for this manager.222#223def close224@transport.close225end226227def load_module(name)228@mib.load_module(name)229end230231##232# Sends a get request for the supplied list of ObjectId or VarBind233# objects.234#235# Returns a Response PDU with the results of the request.236#237def get(object_list)238varbind_list = @mib.varbind_list(object_list, :NullValue)239request = GetRequest.new(@@request_id.next, varbind_list)240try_request(request)241end242243##244# Sends a get request for the supplied list of ObjectId or VarBind245# objects.246#247# Returns a list of the varbind values only, not the entire response,248# in the same order as the initial object_list. This method is249# useful for retrieving scalar values.250#251# For example:252#253# SNMP::Manager.open(:Host => "localhost") do |manager|254# puts manager.get_value("sysDescr.0")255# end256#257def get_value(object_list)258if object_list.respond_to? :to_ary259get(object_list).vb_list.collect { |vb| vb.value }260else261get(object_list).vb_list.first.value262end263end264265##266# Sends a get-next request for the supplied list of ObjectId or VarBind267# objects.268#269# Returns a Response PDU with the results of the request.270#271def get_next(object_list)272varbind_list = @mib.varbind_list(object_list, :NullValue)273request = GetNextRequest.new(@@request_id.next, varbind_list)274try_request(request)275end276277##278# Sends a get-bulk request. The non_repeaters parameter specifies279# the number of objects in the object_list to be retrieved once. The280# remaining objects in the list will be retrieved up to the number of281# times specified by max_repetitions.282#283def get_bulk(non_repeaters, max_repetitions, object_list)284varbind_list = @mib.varbind_list(object_list, :NullValue)285request = GetBulkRequest.new(286@@request_id.next,287varbind_list,288non_repeaters,289max_repetitions)290try_request(request)291end292293##294# Sends a set request using the supplied list of VarBind objects.295#296# Returns a Response PDU with the results of the request.297#298def set(object_list)299varbind_list = @mib.varbind_list(object_list, :KeepValue)300request = SetRequest.new(@@request_id.next, varbind_list)301try_request(request, @write_community)302end303304##305# Sends an SNMPv1 style trap.306#307# enterprise: The enterprise OID from the IANA assigned numbers308# (http://www.iana.org/assignments/enterprise-numbers) as a String or309# an ObjectId.310#311# agent_addr: The IP address of the SNMP agent as a String or IpAddress.312#313# generic_trap: The generic trap identifier. One of :coldStart,314# :warmStart, :linkDown, :linkUp, :authenticationFailure,315# :egpNeighborLoss, or :enterpriseSpecific316#317# specific_trap: An integer representing the specific trap type for318# an enterprise-specific trap.319#320# timestamp: An integer representing the number of hundredths of321# a second that this system has been up.322#323# object_list: A list of additional varbinds to send with the trap.324#325# For example:326#327# Manager.open(:Version => :SNMPv1) do |snmp|328# snmp.trap_v1(329# "enterprises.9",330# "10.1.2.3",331# :enterpriseSpecific,332# 42,333# 12345,334# [VarBind.new("1.3.6.1.2.3.4", Integer.new(1))])335# end336#337def trap_v1(enterprise, agent_addr, generic_trap, specific_trap, timestamp, object_list=[])338vb_list = @mib.varbind_list(object_list, :KeepValue)339ent_oid = @mib.oid(enterprise)340agent_ip = IpAddress.new(agent_addr)341specific_int = Integer(specific_trap)342ticks = TimeTicks.new(timestamp)343trap = SNMPv1_Trap.new(ent_oid, agent_ip, generic_trap, specific_int, ticks, vb_list)344send_request(trap, @community, @host, @trap_port)345end346347##348# Sends an SNMPv2c style trap.349#350# sys_up_time: An integer representing the number of hundredths of351# a second that this system has been up.352#353# trap_oid: An ObjectId or String with the OID identifier for this354# trap.355#356# object_list: A list of additional varbinds to send with the trap.357#358def trap_v2(sys_up_time, trap_oid, object_list=[])359vb_list = create_trap_vb_list(sys_up_time, trap_oid, object_list)360trap = SNMPv2_Trap.new(@@request_id.next, vb_list)361send_request(trap, @community, @host, @trap_port)362end363364##365# Sends an inform request using the supplied varbind list.366#367# sys_up_time: An integer representing the number of hundredths of368# a second that this system has been up.369#370# trap_oid: An ObjectId or String with the OID identifier for this371# inform request.372#373# object_list: A list of additional varbinds to send with the inform.374#375def inform(sys_up_time, trap_oid, object_list=[])376vb_list = create_trap_vb_list(sys_up_time, trap_oid, object_list)377request = InformRequest.new(@@request_id.next, vb_list)378try_request(request, @community, @host, @trap_port)379end380381##382# Helper method for building VarBindList for trap and inform requests.383#384def create_trap_vb_list(sys_up_time, trap_oid, object_list)385vb_args = @mib.varbind_list(object_list, :KeepValue)386uptime_vb = VarBind.new(SNMP::SYS_UP_TIME_OID, TimeTicks.new(sys_up_time.to_int))387trap_vb = VarBind.new(SNMP::SNMP_TRAP_OID_OID, @mib.oid(trap_oid))388VarBindList.new([uptime_vb, trap_vb, *vb_args])389end390391##392# Walks a list of ObjectId or VarBind objects using get_next until393# the response to the first OID in the list reaches the end of its394# MIB subtree.395#396# The varbinds from each get_next are yielded to the given block as397# they are retrieved. The result is yielded as a VarBind when walking398# a single object or as a VarBindList when walking a list of objects.399#400# Normally this method is used for walking tables by providing an401# ObjectId for each column of the table.402#403# For example:404#405# SNMP::Manager.open(:Host => "localhost") do |manager|406# manager.walk("ifTable") { |vb| puts vb }407# end408#409# SNMP::Manager.open(:Host => "localhost") do |manager|410# manager.walk(["ifIndex", "ifDescr"]) do |index, descr|411# puts "#{index.value} #{descr.value}"412# end413# end414#415# The index_column identifies the column that will provide the index416# for each row. This information is used to deal with "holes" in a417# table (when a row is missing a varbind for one column). A missing418# varbind is replaced with a varbind with the value NoSuchInstance.419#420# Note: If you are getting back rows where all columns have a value of421# NoSuchInstance then your index column is probably missing one of the422# rows. Choose an index column that includes all indexes for the table.423#424def walk(object_list, index_column=0)425raise ArgumentError, "expected a block to be given" unless block_given?426vb_list = @mib.varbind_list(object_list, :NullValue)427raise ArgumentError, "index_column is past end of varbind list" if index_column >= vb_list.length428is_single_vb = object_list.respond_to?(:to_str) ||429object_list.respond_to?(:to_varbind)430start_list = vb_list431start_oid = vb_list[index_column].name432last_oid = start_oid433loop do434vb_list = get_next(vb_list).vb_list435index_vb = vb_list[index_column]436break if EndOfMibView == index_vb.value437stop_oid = index_vb.name438if stop_oid <= last_oid439# warn "OIDs are not increasing, #{last_oid} followed by #{stop_oid}"440break441end442break unless stop_oid.subtree_of?(start_oid)443last_oid = stop_oid444if is_single_vb445yield index_vb446else447vb_list = validate_row(vb_list, start_list, index_column)448yield vb_list449end450end451end452453##454# Helper method for walk. Checks all of the VarBinds in vb_list to455# make sure that the row indices match. If the row index does not456# match the index column, then that varbind is replaced with a varbind457# with a value of NoSuchInstance.458#459def validate_row(vb_list, start_list, index_column)460start_vb = start_list[index_column]461index_vb = vb_list[index_column]462row_index = index_vb.name.index(start_vb.name)463vb_list.each_index do |i|464if i != index_column465expected_oid = start_list[i].name + row_index466if vb_list[i].name != expected_oid467vb_list[i] = VarBind.new(expected_oid, NoSuchInstance)468end469end470end471vb_list472end473private :validate_row474475##476# Set the next request-id instead of letting it be generated477# automatically. This method is useful for testing and debugging.478#479def next_request_id=(request_id)480@@request_id.force_next(request_id)481end482483private484485def warn(message)486trace = caller(2)487location = trace[0].sub(/:in.*/,'')488Kernel::warn "#{location}: warning: #{message}"489end490491def load_modules(module_list, mib_dir)492module_list.each { |m| @mib.load_module(m, mib_dir) }493end494495def try_request(request, community=@community, host=@host, port=@port)496(@retries.to_i + 1).times do |n|497send_request(request, community, host, port)498begin499Timeout.timeout(@timeout) do500return get_response(request)501end502rescue Timeout::Error503# no action - try again504end505end506raise RequestTimeout, "host #{@config[:Host]} not responding", caller507end508509def send_request(request, community, host, port)510message = Message.new(@snmp_version, community, request)511@transport.send(message.encode, host, port)512end513514##515# Wait until response arrives. Ignore responses with mismatched IDs;516# these responses are typically from previous requests that timed out517# or almost timed out.518#519def get_response(request)520begin521data = @transport.recv(@max_bytes)522message = Message.decode(data)523response = message.pdu524end until request.request_id == response.request_id525response526end527end528529class UDPServerTransport530def initialize(host, port)531@socket = UDPSocket.open532@socket.bind(host, port)533end534535def close536@socket.close537end538539def send(data, host, port)540@socket.send(data, 0, host, port)541end542543def recvfrom(max_bytes)544data, host_info = @socket.recvfrom(max_bytes)545flags, host_port, host_name, host_ip = host_info546return data, host_ip, host_port547end548end549550##551# == SNMP Trap Listener552#553# Listens to a socket and processes received traps and informs in a separate554# thread.555#556# === Example557#558# require 'snmp'559#560# m = SNMP::TrapListener.new(:Port => 1062, :Community => 'public') do |manager|561# manager.on_trap_default { |trap| p trap }562# end563# m.join564#565class TrapListener566DefaultConfig = {567:Host => 'localhost',568:Port => 162,569:Community => 'public',570:ServerTransport => UDPServerTransport,571:MaxReceiveBytes => 8000}572573NULL_HANDLER = Proc.new {}574575##576# Start a trap handler thread. If a block is provided then the block577# is executed before trap handling begins. This block is typically used578# to define the trap handler blocks.579#580# The trap handler blocks execute in the context of the trap handler thread.581#582# The most specific trap handler is executed when a trap arrives. Only one583# handler is executed. The handlers are checked in the following order:584#585# 1. handler for a specific OID586# 2. handler for a specific SNMP version587# 3. default handler588#589def initialize(config={}, &block)590@config = DefaultConfig.dup.update(config)591@transport = @config[:ServerTransport].new(@config[:Host], @config[:Port])592@max_bytes = @config[:MaxReceiveBytes]593@handler_init = block594@oid_handler = {}595@v1_handler = nil596@v2c_handler = nil597@default_handler = nil598@lock = Mutex.new599@handler_thread = Thread.new(self) { |m| process_traps(m) }600end601602##603# Define the default trap handler. The default trap handler block is604# executed only if no other block is applicable. This handler should605# expect to receive both SNMPv1_Trap and SNMPv2_Trap objects.606#607def on_trap_default(&block)608raise ArgumentError, "a block must be provided" unless block609@lock.synchronize { @default_handler = block }610end611612##613# Define a trap handler block for a specific trap ObjectId. This handler614# only applies to SNMPv2 traps. Note that symbolic OIDs are not615# supported by this method (like in the SNMP.Manager class).616#617def on_trap(object_id, &block)618raise ArgumentError, "a block must be provided" unless block619@lock.synchronize { @oid_handler[ObjectId.new(object_id)] = block }620end621622##623# Define a trap handler block for all SNMPv1 traps. The trap yielded624# to the block will always be an SNMPv1_Trap.625#626def on_trap_v1(&block)627raise ArgumentError, "a block must be provided" unless block628@lock.synchronize { @v1_handler = block }629end630631##632# Define a trap handler block for all SNMPv2c traps. The trap yielded633# to the block will always be an SNMPv2_Trap. Note that InformRequest634# is a subclass of SNMPv2_Trap, so inform PDUs are also received by635# this handler.636#637def on_trap_v2c(&block)638raise ArgumentError, "a block must be provided" unless block639@lock.synchronize { @v2c_handler = block }640end641642##643# Joins the current thread to the trap handler thread.644#645# See also Thread#join.646#647def join648@handler_thread.join649end650651##652# Stops the trap handler thread and releases the socket.653#654# See also Thread#exit.655#656def exit657@handler_thread.exit658@transport.close659end660661alias kill exit662alias terminate exit663664private665666def process_traps(trap_listener)667@handler_init.call(trap_listener) if @handler_init668loop do669data, source_ip, source_port = @transport.recvfrom(@max_bytes)670begin671message = Message.decode(data)672if @config[:Community] == message.community673trap = message.pdu674if trap.kind_of?(InformRequest)675@transport.send(message.response.encode, source_ip, source_port)676end677trap.source_ip = source_ip678select_handler(trap).call(trap)679end680rescue => e681puts "Error handling trap: #{e}"682puts e.backtrace.join("\n")683puts "Received data:"684p data685end686end687end688689def select_handler(trap)690@lock.synchronize do691if trap.kind_of?(SNMPv2_Trap)692oid = trap.trap_oid693if @oid_handler[oid]694return @oid_handler[oid]695elsif @v2c_handler696return @v2c_handler697elsif @default_handler698return @default_handler699else700return NULL_HANDLER701end702elsif trap.kind_of?(SNMPv1_Trap)703if @v1_handler704return @v1_handler705elsif @default_handler706return @default_handler707else708return NULL_HANDLER709end710else711return NULL_HANDLER712end713end714end715end716717end718719720