580 lines
12 KiB
Ruby
580 lines
12 KiB
Ruby
#
|
|
# Copyright (c) 2004 David R. Halliday
|
|
# All rights reserved.
|
|
#
|
|
# This SNMP library is free software. Redistribution is permitted under the
|
|
# same terms and conditions as the standard Ruby distribution. See the
|
|
# COPYING file in the Ruby distribution for details.
|
|
#
|
|
|
|
require 'snmp/ber'
|
|
|
|
include SNMP::BER
|
|
|
|
module SNMP
|
|
|
|
class UnsupportedValueTag < RuntimeError; end
|
|
class InvalidIpAddress < ArgumentError; end
|
|
|
|
class VarBindList < Array
|
|
def self.decode(data)
|
|
list = VarBindList.new
|
|
varbind_data, remainder = decode_sequence(data)
|
|
while varbind_data != ""
|
|
varbind, varbind_data = VarBind.decode(varbind_data)
|
|
list << varbind
|
|
end
|
|
return list, remainder
|
|
end
|
|
|
|
def initialize(varbind_list=[])
|
|
super()
|
|
if varbind_list.respond_to? :to_str
|
|
self << ObjectId.new(varbind_list.to_str).to_varbind
|
|
elsif varbind_list.respond_to? :to_varbind
|
|
self << varbind_list.to_varbind
|
|
else
|
|
varbind_list.each do |item|
|
|
if item.respond_to? :to_str
|
|
self << ObjectId.new(item.to_str).to_varbind
|
|
else
|
|
self << item.to_varbind
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def asn1_type
|
|
"VarBindList"
|
|
end
|
|
|
|
def encode
|
|
varbind_data = ""
|
|
self.each do |varbind|
|
|
varbind_data << varbind.encode
|
|
end
|
|
encode_sequence(varbind_data)
|
|
end
|
|
end
|
|
|
|
class Integer
|
|
include Comparable
|
|
|
|
def self.decode(value_data)
|
|
Integer.new(decode_integer_value(value_data))
|
|
end
|
|
|
|
def asn1_type
|
|
"INTEGER"
|
|
end
|
|
|
|
def initialize(value)
|
|
@value = value.to_i
|
|
end
|
|
|
|
def <=>(other)
|
|
@value <=> other.to_i
|
|
end
|
|
|
|
def coerce(other)
|
|
if other.kind_of? Fixnum
|
|
return [other, @value]
|
|
else
|
|
return [other.to_f, self.to_f]
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
@value.to_s
|
|
end
|
|
|
|
def to_i
|
|
@value
|
|
end
|
|
|
|
def to_f
|
|
@value.to_f
|
|
end
|
|
|
|
def encode
|
|
encode_integer(@value)
|
|
end
|
|
|
|
def to_oid
|
|
raise RangeError, "@{value} cannot be an OID (must be >0)" if @value < 0
|
|
ObjectId.new([@value])
|
|
end
|
|
end
|
|
|
|
class Integer32 < Integer
|
|
def initialize(value)
|
|
super(value)
|
|
raise ArgumentError, "Out of range: #{value}" if value < -2147483648
|
|
raise ArgumentError, "Out of range: #{value}" if value > 2147483647
|
|
end
|
|
end
|
|
|
|
class OctetString < String
|
|
def self.decode(value_data)
|
|
OctetString.new(value_data)
|
|
end
|
|
|
|
def asn1_type
|
|
"OCTET STRING"
|
|
end
|
|
|
|
def encode
|
|
encode_octet_string(self)
|
|
end
|
|
|
|
def to_oid
|
|
oid = ObjectId.new
|
|
each_byte { |b| oid << b }
|
|
oid
|
|
end
|
|
end
|
|
|
|
class ObjectId < Array
|
|
include Comparable
|
|
|
|
def self.decode(value_data)
|
|
ObjectId.new(decode_object_id_value(value_data))
|
|
end
|
|
|
|
def asn1_type
|
|
"OBJECT IDENTIFIER"
|
|
end
|
|
|
|
##
|
|
# Create an object id. The input is expected to be either a string
|
|
# in the format "n.n.n.n.n.n" or an array of integers.
|
|
#
|
|
def initialize(id=[])
|
|
if id.nil?
|
|
raise ArgumentError
|
|
elsif id.respond_to? :to_str
|
|
super(make_integers(id.to_str.split(".")))
|
|
else
|
|
super(make_integers(id.to_ary))
|
|
end
|
|
rescue ArgumentError
|
|
raise ArgumentError, "#{id.inspect}:#{id.class} not a valid object ID"
|
|
end
|
|
|
|
def to_varbind
|
|
VarBind.new(self, Null)
|
|
end
|
|
|
|
def to_oid
|
|
self
|
|
end
|
|
|
|
def to_s
|
|
self.join('.')
|
|
end
|
|
|
|
def inspect
|
|
"[#{self.to_s}]"
|
|
end
|
|
|
|
def encode
|
|
encode_object_id(self)
|
|
end
|
|
|
|
##
|
|
# Returns true if this ObjectId is a subtree of the provided parent tree
|
|
# ObjectId. For example, "1.3.6.1.5" is a subtree of "1.3.6.1".
|
|
#
|
|
def subtree_of?(parent_tree)
|
|
parent_tree = make_object_id(parent_tree)
|
|
if parent_tree.length > self.length
|
|
false
|
|
else
|
|
parent_tree.each_index do |i|
|
|
return false if parent_tree[i] != self[i]
|
|
end
|
|
true
|
|
end
|
|
end
|
|
|
|
##
|
|
# Returns an index based on the difference between this ObjectId
|
|
# and the provided parent ObjectId.
|
|
#
|
|
# For example, ObjectId.new("1.3.6.1.5").index("1.3.6.1") returns an
|
|
# ObjectId of "5".
|
|
#
|
|
def index(parent_tree)
|
|
parent_tree = make_object_id(parent_tree)
|
|
if not subtree_of?(parent_tree)
|
|
raise ArgumentError, "#{self.to_s} not a subtree of #{parent_tree.to_s}"
|
|
elsif self.length == parent_tree.length
|
|
raise ArgumentError, "OIDs are the same"
|
|
else
|
|
ObjectId.new(self[parent_tree.length..-1])
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def make_integers(list)
|
|
list.collect{|n| Integer(n)}
|
|
end
|
|
|
|
def make_object_id(oid)
|
|
oid.kind_of?(ObjectId) ? oid : ObjectId.new(oid)
|
|
end
|
|
|
|
end
|
|
|
|
class IpAddress
|
|
class << self
|
|
def decode(value_data)
|
|
IpAddress.new(value_data)
|
|
end
|
|
end
|
|
|
|
def asn1_type
|
|
"IpAddress"
|
|
end
|
|
|
|
##
|
|
# Create an IpAddress object. The constructor accepts either a raw
|
|
# four-octet string or a formatted string of integers separated by dots
|
|
# (i.e. "10.1.2.3").
|
|
#
|
|
def initialize(value_data)
|
|
ip = value_data.to_str
|
|
if ip.length > 4
|
|
ip = parse_string(ip)
|
|
elsif ip.length != 4
|
|
raise InvalidIpAddress, "Expected 4 octets or formatted string, got #{value_data.inspect}"
|
|
end
|
|
@value = ip
|
|
end
|
|
|
|
##
|
|
# Returns a raw four-octet string representing this IpAddress.
|
|
#
|
|
def to_str
|
|
@value.dup
|
|
end
|
|
|
|
##
|
|
# Returns a formatted, dot-separated string representing this IpAddress.
|
|
#
|
|
def to_s
|
|
octets = []
|
|
@value.each_byte { |b| octets << b.to_s }
|
|
octets.join('.')
|
|
end
|
|
|
|
def to_oid
|
|
oid = ObjectId.new
|
|
@value.each_byte { |b| oid << b }
|
|
oid
|
|
end
|
|
|
|
def ==(other)
|
|
if other.respond_to? :to_str
|
|
return @value.eql?(other.to_str)
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def eql?(other)
|
|
self == other
|
|
end
|
|
|
|
def hash
|
|
@value.hash
|
|
end
|
|
|
|
def encode
|
|
encode_tlv(IpAddress_TAG, @value)
|
|
end
|
|
|
|
private
|
|
def parse_string(ip_string)
|
|
parts = ip_string.split(".")
|
|
raise InvalidIpAddress, ip_string.inspect if parts.length != 4
|
|
value_data = ""
|
|
parts.each do |s|
|
|
octet = s.to_i
|
|
raise InvalidIpAddress, ip_string.inspect if octet > 255
|
|
raise InvalidIpAddress, ip_string.inspect if octet < 0
|
|
value_data << octet.chr
|
|
end
|
|
value_data
|
|
end
|
|
|
|
end
|
|
|
|
class UnsignedInteger < Integer
|
|
def initialize(value)
|
|
super(value)
|
|
raise ArgumentError, "Negative integer invalid: #{value}" if value < 0
|
|
raise ArgumentError, "Out of range: #{value}" if value > 4294967295
|
|
end
|
|
|
|
def self.decode(value_data)
|
|
self.new(decode_uinteger_value(value_data))
|
|
end
|
|
end
|
|
|
|
class Counter32 < UnsignedInteger
|
|
def asn1_type
|
|
"Counter32"
|
|
end
|
|
|
|
def encode
|
|
encode_tagged_integer(Counter32_TAG, @value)
|
|
end
|
|
end
|
|
|
|
class Gauge32 < UnsignedInteger
|
|
def asn1_type
|
|
"Gauge32"
|
|
end
|
|
|
|
def encode
|
|
encode_tagged_integer(Gauge32_TAG, @value)
|
|
end
|
|
end
|
|
|
|
class Unsigned32 < UnsignedInteger
|
|
def asn1_type
|
|
"Unsigned32"
|
|
end
|
|
|
|
def encode
|
|
encode_tagged_integer(Unsigned32_TAG, @value)
|
|
end
|
|
end
|
|
|
|
class TimeTicks < UnsignedInteger
|
|
def asn1_type
|
|
"TimeTicks"
|
|
end
|
|
|
|
def encode
|
|
encode_tagged_integer(TimeTicks_TAG, @value)
|
|
end
|
|
|
|
def to_s
|
|
days, remainder = @value.divmod(8640000)
|
|
hours, remainder = remainder.divmod(360000)
|
|
minutes, remainder = remainder.divmod(6000)
|
|
seconds, hundredths = remainder.divmod(100)
|
|
case
|
|
when days < 1
|
|
sprintf('%02d:%02d:%02d.%02d',
|
|
hours, minutes, seconds, hundredths)
|
|
when days == 1
|
|
sprintf('1 day, %02d:%02d:%02d.%02d',
|
|
hours, minutes, seconds, hundredths)
|
|
when days > 1
|
|
sprintf('%d days, %02d:%02d:%02d.%02d',
|
|
days, hours, minutes, seconds, hundredths)
|
|
end
|
|
end
|
|
end
|
|
|
|
class Opaque < OctetString
|
|
def self.decode(value_data)
|
|
Opaque.new(value_data)
|
|
end
|
|
|
|
def asn1_type
|
|
"Opaque"
|
|
end
|
|
|
|
def encode
|
|
encode_tlv(Opaque_TAG, self)
|
|
end
|
|
end
|
|
|
|
class Counter64 < Integer
|
|
def self.decode(value_data)
|
|
Counter64.new(decode_integer_value(value_data))
|
|
end
|
|
|
|
def asn1_type
|
|
"Counter64"
|
|
end
|
|
|
|
def initialize(value)
|
|
super(value)
|
|
raise ArgumentError, "Negative integer invalid: #{value}" if value < 0
|
|
raise ArgumentError, "Out of range: #{value}" if value > 18446744073709551615
|
|
end
|
|
|
|
def encode
|
|
encode_tagged_integer(Counter64_TAG, @value)
|
|
end
|
|
end
|
|
|
|
class Null
|
|
class << self
|
|
def decode(value_data)
|
|
Null
|
|
end
|
|
|
|
def encode
|
|
encode_null
|
|
end
|
|
|
|
def asn1_type
|
|
'Null'
|
|
end
|
|
|
|
def to_s
|
|
asn1_type
|
|
end
|
|
end
|
|
end
|
|
|
|
class NoSuchObject
|
|
class << self
|
|
def decode(value_data)
|
|
NoSuchObject
|
|
end
|
|
|
|
def encode
|
|
encode_exception(NoSuchObject_TAG)
|
|
end
|
|
|
|
def asn1_type
|
|
'noSuchObject'
|
|
end
|
|
|
|
def to_s
|
|
asn1_type
|
|
end
|
|
end
|
|
end
|
|
|
|
class NoSuchInstance
|
|
class << self
|
|
def decode(value_data)
|
|
NoSuchInstance
|
|
end
|
|
|
|
def encode
|
|
encode_exception(NoSuchInstance_TAG)
|
|
end
|
|
|
|
def asn1_type
|
|
'noSuchInstance'
|
|
end
|
|
|
|
def to_s
|
|
asn1_type
|
|
end
|
|
end
|
|
end
|
|
|
|
class EndOfMibView
|
|
class << self
|
|
def decode(value_data)
|
|
EndOfMibView
|
|
end
|
|
|
|
def encode
|
|
encode_exception(EndOfMibView_TAG)
|
|
end
|
|
|
|
def asn1_type
|
|
'endOfMibView'
|
|
end
|
|
|
|
def to_s
|
|
asn1_type
|
|
end
|
|
end
|
|
end
|
|
|
|
class VarBind
|
|
attr_accessor :name
|
|
attr_accessor :value
|
|
|
|
alias :oid :name
|
|
|
|
class << self
|
|
def decode(data)
|
|
varbind_data, remaining_varbind_data = decode_sequence(data)
|
|
name, remainder = decode_object_id(varbind_data)
|
|
value, remainder = decode_value(remainder)
|
|
assert_no_remainder(remainder)
|
|
return VarBind.new(name, value), remaining_varbind_data
|
|
end
|
|
|
|
ValueDecoderMap = {
|
|
INTEGER_TAG => Integer,
|
|
OCTET_STRING_TAG => OctetString,
|
|
NULL_TAG => Null,
|
|
OBJECT_IDENTIFIER_TAG => ObjectId,
|
|
IpAddress_TAG => IpAddress,
|
|
Counter32_TAG => Counter32,
|
|
Gauge32_TAG => Gauge32,
|
|
# note Gauge32 tag same as Unsigned32
|
|
TimeTicks_TAG => TimeTicks,
|
|
Opaque_TAG => Opaque,
|
|
Counter64_TAG => Counter64,
|
|
NoSuchObject_TAG => NoSuchObject,
|
|
NoSuchInstance_TAG => NoSuchInstance,
|
|
EndOfMibView_TAG => EndOfMibView
|
|
}
|
|
|
|
def decode_value(data)
|
|
value_tag, value_data, remainder = decode_tlv(data)
|
|
decoder_class = ValueDecoderMap[value_tag]
|
|
if decoder_class
|
|
value = decoder_class.decode(value_data)
|
|
else
|
|
raise UnsupportedValueTag, value_tag.to_s
|
|
end
|
|
return value, remainder
|
|
end
|
|
end
|
|
|
|
def initialize(name, value=Null)
|
|
if name.kind_of? ObjectId
|
|
@name = name
|
|
else
|
|
@name = ObjectName.new(name)
|
|
end
|
|
@value = value
|
|
end
|
|
|
|
def asn1_type
|
|
"VarBind"
|
|
end
|
|
|
|
def to_varbind
|
|
self
|
|
end
|
|
|
|
def to_s
|
|
"[name=#{@name.to_s}, value=#{@value.to_s} (#{@value.asn1_type})]"
|
|
end
|
|
|
|
def each
|
|
yield self
|
|
end
|
|
|
|
def encode
|
|
data = encode_object_id(@name) << value.encode
|
|
encode_sequence(data)
|
|
end
|
|
end
|
|
|
|
class ObjectName < ObjectId
|
|
def asn1_type
|
|
"ObjectName"
|
|
end
|
|
end
|
|
|
|
end
|