define_type metaprogramming. Auto-detect l3addr family. More comments

This commit is contained in:
Brian Candler
2011-04-30 21:46:36 +01:00
parent fb65653159
commit 3d4be4cd58

View File

@@ -2,6 +2,10 @@ require 'netlink/constants'
require 'ipaddr' require 'ipaddr'
module Netlink module Netlink
EMPTY_STRING = "".freeze #:nodoc:
EMPTY_ARRAY = [].freeze #:nodoc:
# struct rtnl_link_stats / rtnl_link_stats64
LinkStats = Struct.new :rx_packets, :tx_packets, LinkStats = Struct.new :rx_packets, :tx_packets,
:rx_bytes, :tx_bytes, :rx_bytes, :tx_bytes,
:rx_errors, :tx_errors, :rx_errors, :tx_errors,
@@ -15,57 +19,86 @@ module Netlink
:tx_window_errors, :tx_window_errors,
:rx_compressed, :tx_compressed :rx_compressed, :tx_compressed
# struct rta_cacheinfo
RTACacheInfo = Struct.new :clntref, :lastuse, :expires, :error, :used, :id, :ts, :tsage RTACacheInfo = Struct.new :clntref, :lastuse, :expires, :error, :used, :id, :ts, :tsage
# struct ifa_cacheinfo
IFACacheInfo = Struct.new :prefered, :valid, :cstamp, :tstamp IFACacheInfo = Struct.new :prefered, :valid, :cstamp, :tstamp
LinkIFMap = Struct.new :mem_start, :mem_end, :base_addr, :irq, :dma, :port # struct ifmap
IFMap = Struct.new :mem_start, :mem_end, :base_addr, :irq, :dma, :port
# Base class for Netlink messages # This is the base class from which all Netlink messages are derived.
# To define a new Netlink message, make a subclass and then call the
# "field" metaprogramming method to define the parts of the message, in
# order. The "code" metaprogramming method defines which incoming message
# types are to be built using this structure.
#
# You can then instantiate the message by calling 'new' as usual. It is
# usually most convenient to pass in a hash of values when creating a message.
#
# class Foo < Message
# code 10, 11, 12
# field :foo, :char, :default=>255
# field :bar, :long
# end
# msg = Foo.new(:bar => 123) # or Foo.new("bar" => 123)
# msg.bar = 456
# msg2 = Foo.new(:qux => 999) # error: no method "qux="
# msg2 = Foo.new(msg) # cloning an existing message
#
# Use RtattrMessage for messages which are followed by variable rtattrs.
class Message class Message
# Map of numeric message type code => message class TYPE_INFO = {} #:nodoc
CODE_TO_MESSAGE = {}
# Define a new type for use with field and rtattr. You supply the
# symbolic name for the type, and a set of options. field supports only:
# :pattern => "str" # format string for Array#pack / String#unpack
# :default => val # default (if not 0)
# rtattr optionally also supports:
# :pack => lambda # code to convert value to binary string
# :unpack => lambda # code to convert binary string to value
def self.define_type(name, opt)
TYPE_INFO[name] = opt
end
define_type :uchar, :pattern => "C"
define_type :uint16, :pattern => "S"
define_type :uint32, :pattern => "L"
define_type :char, :pattern => "c"
define_type :int16, :pattern => "s"
define_type :int32, :pattern => "l"
define_type :ushort, :pattern => "S_"
define_type :uint, :pattern => "I"
define_type :ulong, :pattern => "L_"
define_type :short, :pattern => "s_"
define_type :int, :pattern => "i"
define_type :long, :pattern => "l_"
define_type :binary, :pattern => "a*", :default => EMPTY_STRING
define_type :cstring, :pattern => "Z*", :default => EMPTY_STRING
define_type :linkstats32,
:pack => lambda { |val,obj| val.to_a.pack("L23") },
:unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("L23"))) }
define_type :linkstats64,
:pack => lambda { |val,obj| val.to_a.pack("Q23") },
:unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("Q23"))) }
define_type :rta_cacheinfo,
:pack => lambda { |val,obj| val.to_a.pack("L*") },
:unpack => lambda { |str,obj| RTACacheInfo.new(*(str.unpack("L*"))) }
define_type :ifa_cacheinfo,
:pack => lambda { |val,obj| val.to_a.pack("L*") },
:unpack => lambda { |str,obj| IFACacheInfo.new(*(str.unpack("L*"))) }
IFMAP_PACK = "QQQSCC".freeze #:nodoc:
define_type :ifmap,
:pack => lambda { |val,obj| val.to_a.pack(IFMAP_PACK) },
:unpack => lambda { |str,obj| IFMap.new(*(str.unpack(IFMAP_PACK))) }
METRIC_PACK = "SSL".freeze #:nodoc: METRIC_PACK = "SSL".freeze #:nodoc:
METRIC_SIZE = [0,0,0].pack(METRIC_PACK).bytesize #:nodoc: METRIC_SIZE = [0,0,0].pack(METRIC_PACK).bytesize #:nodoc:
define_type :metrics,
IFMAP_PACK = "QQQSCC".freeze #:nodoc:
# Defines each of the possible field types
TYPE_INFO = {
:uchar => { :pattern => "C" },
:uint16 => { :pattern => "S" },
:uint32 => { :pattern => "L" },
:char => { :pattern => "c" },
:int16 => { :pattern => "s" },
:int32 => { :pattern => "l" },
:ushort => { :pattern => "S_" },
:uint => { :pattern => "I" },
:ulong => { :pattern => "L_" },
:short => { :pattern => "s_" },
:int => { :pattern => "i" },
:long => { :pattern => "l_" },
:binary => { :pattern => "a*", :default => "".freeze },
:cstring => { :pattern => "Z*", :default => "".freeze },
:stats32 => {
:pack => lambda { |val,obj| val.to_a.pack("L23") },
:unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("L23"))) },
},
:stats64 => {
:pack => lambda { |val,obj| val.to_a.pack("Q23") },
:unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("Q23"))) },
},
:rta_cacheinfo => {
:pack => lambda { |val,obj| val.to_a.pack("L*") },
:unpack => lambda { |str,obj| RTACacheInfo.new(*(str.unpack("L*"))) },
},
:ifa_cacheinfo => {
:pack => lambda { |val,obj| val.to_a.pack("L*") },
:unpack => lambda { |str,obj| IFACacheInfo.new(*(str.unpack("L*"))) },
},
:ifmap => {
:pack => lambda { |val,obj| val.to_a.pack(IFMAP_PACK) },
:unpack => lambda { |str,obj| LinkIFMap.new(*(str.unpack(IFMAP_PACK))) },
},
:metrics => {
:pack => lambda { |metrics,obj| :pack => lambda { |metrics,obj|
metrics.map { |code,val| [METRIC_SIZE,code,val].pack(METRIC_PACK) }.join metrics.map { |code,val| [METRIC_SIZE,code,val].pack(METRIC_PACK) }.join
}, },
@@ -73,13 +106,28 @@ module Netlink
res = {} # in kernel the dst.metrics structure is array of u32 res = {} # in kernel the dst.metrics structure is array of u32
RtattrMessage.unpack_rtattr(str) { |code,val| res[code] = val.unpack("L").first } RtattrMessage.unpack_rtattr(str) { |code,val| res[code] = val.unpack("L").first }
res res
}, }
},
:l2addr => { # L2 addresses are presented as ASCII hex. You may optionally include
:pack => lambda { |val,obj| Array(val).pack("H*") }, # colons, hyphens or dots.
:unpack => lambda { |val,obj| val.unpack("H*").first }, # Link.new(:address => "00:11:22:33:44:55") # this is OK
}, define_type :l2addr,
:l3addr => { :pack => lambda { |val,obj| [val.delete(":-.")].pack("H*") },
:unpack => lambda { |val,obj| val.unpack("H*").first }
# L3 addresses are presented as IPAddr objects where possible. When
# setting an address, you may provide an IPAddr object, an IP in readable
# string form, or an integer. All of the following are acceptable:
# Addr.new(:family=>Socket::AF_INET, :address=>IPAddr.new("1.2.3.4"))
# Addr.new(:family=>Socket::AF_INET, :address=>"1.2.3.4")
# Addr.new(:family=>Socket::AF_INET, :address=>0x01020304)
# Furthermore, the 'family' will be set automatically if it is unset
# at the time the message is encoded:
# Addr.new(:address=>IPAddr.new("1.2.3.4")).to_s # ok
# Addr.new(:address=>"1.2.3.4").to_s # ok
# Addr.new(:address=>0x01020304).to_s # error, unknown family
# Addr.new(:address=>"1.2.3.4", :local=>"::1").to_s # error, mismatched families
define_type :l3addr,
:pack => lambda { |val,obj| :pack => lambda { |val,obj|
case obj.family case obj.family
when Socket::AF_INET, Socket::AF_INET6 when Socket::AF_INET, Socket::AF_INET6
@@ -91,10 +139,21 @@ module Netlink
else else
IPAddr.new(val) IPAddr.new(val)
end end
raise "Mismatched address family" unless obj.family == ip.family raise "Mismatched address family" unless ip.family == obj.family
ip.hton
when nil, Socket::AF_UNSPEC
ip = case val
when IPAddr
val
when Integer
raise "Missing address family"
else
IPAddr.new(val)
end
obj.family = ip.family
ip.hton ip.hton
else else
raise "Missing or mismatched address family" if val.is_a?(IPAddr) raise "Mismatched address family" if val.is_a?(IPAddr)
val val
end end
}, },
@@ -105,20 +164,8 @@ module Netlink
else else
val val
end end
},
},
} }
# You can initialize a message from a Hash or from another
# instance of itself.
#
# class Foo < Message
# field :foo, :char, :default=>255
# field :bar, :long
# end
# msg = Foo.new(:bar => 123) # or ("bar" => 123)
# msg2 = Foo.new(msg)
# msg3 = Foo.new(:qux => 999) # error, no method "qux="
def initialize(h=nil) def initialize(h=nil)
if h.instance_of?(self.class) if h.instance_of?(self.class)
@attrs = h.to_hash.dup @attrs = h.to_hash.dup
@@ -152,13 +199,18 @@ module Netlink
subclass.const_set(:DEFAULTS, {}) subclass.const_set(:DEFAULTS, {})
end end
# Define which message type code(s) use this structure # Map of numeric message type code => message class
CODE_TO_MESSAGE = {}
# Define which message type code(s) to build using this structure
def self.code(*codes) def self.code(*codes)
codes.each { |code| CODE_TO_MESSAGE[code] = self } codes.each { |code| CODE_TO_MESSAGE[code] = self }
end end
# Define a field for this message, which creates accessor methods and # Define a field for this message, which creates accessor methods and
# sets up data required to pack and unpack the structure. # sets up data required to pack and unpack the structure.
# field :foo, :uchar
# field :foo, :uchar, :default=>0xff # use this default value
def self.field(name, type, opt={}) def self.field(name, type, opt={})
info = TYPE_INFO[type] info = TYPE_INFO[type]
self::FIELDS << name self::FIELDS << name
@@ -207,9 +259,11 @@ module Netlink
end end
end end
# This is a class for a Message which is followed by Rtattr key/value pairs. # Extends Message to support variable Rtattr attributes. Use 'field'
# We assume that any particular attribute is not repeated, so it maps to # to define the fixed parts of the message, and 'rtattr' to define the
# a single attribute in the underlying hash. # permitted rtattrs. We assume that any particular rtattr is not repeated,
# so we store them in the same underlying hash and create simple accessors
# for them.
class RtattrMessage < Message class RtattrMessage < Message
RTATTR_PACK = "S_S_".freeze #:nodoc: RTATTR_PACK = "S_S_".freeze #:nodoc:
RTATTR_SIZE = [0,0].pack(RTATTR_PACK).bytesize #:nodoc: RTATTR_SIZE = [0,0].pack(RTATTR_PACK).bytesize #:nodoc:
@@ -219,6 +273,10 @@ module Netlink
subclass.const_set(:RTATTRS, {}) subclass.const_set(:RTATTRS, {})
end end
# Define an rtattr. You need to provide the code, and optionally the
# type (if not provided, it will just be returned as a raw binary string)
# rtattr :foo, 12
# rtattr :foo, 12, :uint
def self.rtattr(name, code, type=nil, opt={}) def self.rtattr(name, code, type=nil, opt={})
info = TYPE_INFO[type] info = TYPE_INFO[type]
self::RTATTRS[code] = [name, info] self::RTATTRS[code] = [name, info]
@@ -230,15 +288,20 @@ module Netlink
end end
end end
def self.attr_offset #:nodoc: # Return the byte offset to the first rtattr
def self.attr_offset
@attr_offset ||= Message.align(new.to_s.bytesize) @attr_offset ||= Message.align(new.to_s.bytesize)
end end
# Returns the packed binary representation of this message.
# The main message is processed *after* the rtattrs; this is so that
# the address family can be set automatically while processing any
# optional l3 address rtattrs.
def to_s def to_s
data = super data = ""
self.class::RTATTRS.each do |code, (name, info)| self.class::RTATTRS.each do |code, (name, info)|
if val = @attrs[name] if val = @attrs[name]
Message.pad(data) Message.pad(data) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
if pack = info[:pack] if pack = info[:pack]
val = pack[val,self] val = pack[val,self]
elsif pattern = info[:pattern] elsif pattern = info[:pattern]
@@ -247,9 +310,12 @@ module Netlink
data << [val.bytesize+RTATTR_SIZE, code].pack(RTATTR_PACK) << val data << [val.bytesize+RTATTR_SIZE, code].pack(RTATTR_PACK) << val
end end
end end
data data.empty? ? super : Message.pad(super) + data
end end
# Convert a binary representation of this message into an object instance.
# The main message is processed *before* the rtattrs, so that the
# address family is available for l3 address rtattrs.
def self.parse(data) def self.parse(data)
res = super res = super
attrs = res.to_hash attrs = res.to_hash
@@ -273,6 +339,7 @@ module Netlink
res res
end end
# Unpack a string containing a sequence of rtattrs, yielding each in turn.
def self.unpack_rtattr(data, ptr=0) #:nodoc: def self.unpack_rtattr(data, ptr=0) #:nodoc:
while ptr < data.bytesize while ptr < data.bytesize
raise "Truncated rtattr header!" if ptr + RTATTR_SIZE > data.bytesize raise "Truncated rtattr header!" if ptr + RTATTR_SIZE > data.bytesize
@@ -280,7 +347,7 @@ module Netlink
raise "Truncated rtattr body!" if ptr + len > data.bytesize raise "Truncated rtattr body!" if ptr + len > data.bytesize
raise "Invalid rtattr len!" if len < RTATTR_SIZE raise "Invalid rtattr len!" if len < RTATTR_SIZE
yield code, data[ptr+RTATTR_SIZE, len-RTATTR_SIZE] yield code, data[ptr+RTATTR_SIZE, len-RTATTR_SIZE]
ptr = Message.align(ptr + len) ptr = Message.align(ptr + len) # assume NLMSG_ALIGNTO == NLA_ALIGNTO
end end
end end
end end
@@ -299,7 +366,7 @@ module Netlink
rtattr :mtu, IFLA_MTU, :uint32 rtattr :mtu, IFLA_MTU, :uint32
rtattr :link, IFLA_LINK, :int32 rtattr :link, IFLA_LINK, :int32
rtattr :qdisc, IFLA_QDISC, :cstring rtattr :qdisc, IFLA_QDISC, :cstring
rtattr :stats32, IFLA_STATS, :stats32 rtattr :stats32, IFLA_STATS, :linkstats32
rtattr :cost, IFLA_COST rtattr :cost, IFLA_COST
rtattr :master, IFLA_MASTER, :uint32 rtattr :master, IFLA_MASTER, :uint32
rtattr :wireless, IFLA_WIRELESS rtattr :wireless, IFLA_WIRELESS
@@ -314,7 +381,7 @@ module Netlink
rtattr :ifalias, IFLA_IFALIAS, :cstring rtattr :ifalias, IFLA_IFALIAS, :cstring
rtattr :num_vf, IFLA_NUM_VF, :uint32 rtattr :num_vf, IFLA_NUM_VF, :uint32
rtattr :vfinfo_list, IFLA_VFINFO_LIST rtattr :vfinfo_list, IFLA_VFINFO_LIST
rtattr :stats64, IFLA_STATS64, :stats64 rtattr :stats64, IFLA_STATS64, :linkstats64
rtattr :vf_ports, IFLA_VF_PORTS rtattr :vf_ports, IFLA_VF_PORTS
rtattr :port_self, IFLA_PORT_SELF rtattr :port_self, IFLA_PORT_SELF