From 3d4be4cd5826170ddd38dc68ccbe8f238c3220e3 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Sat, 30 Apr 2011 21:46:36 +0100 Subject: [PATCH] define_type metaprogramming. Auto-detect l3addr family. More comments --- lib/netlink/message.rb | 225 ++++++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 79 deletions(-) diff --git a/lib/netlink/message.rb b/lib/netlink/message.rb index 7bebed7..15d7089 100644 --- a/lib/netlink/message.rb +++ b/lib/netlink/message.rb @@ -2,6 +2,10 @@ require 'netlink/constants' require 'ipaddr' module Netlink + EMPTY_STRING = "".freeze #:nodoc: + EMPTY_ARRAY = [].freeze #:nodoc: + + # struct rtnl_link_stats / rtnl_link_stats64 LinkStats = Struct.new :rx_packets, :tx_packets, :rx_bytes, :tx_bytes, :rx_errors, :tx_errors, @@ -15,71 +19,115 @@ module Netlink :tx_window_errors, :rx_compressed, :tx_compressed + # struct rta_cacheinfo RTACacheInfo = Struct.new :clntref, :lastuse, :expires, :error, :used, :id, :ts, :tsage + # struct ifa_cacheinfo 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 - # Map of numeric message type code => message class - CODE_TO_MESSAGE = {} + TYPE_INFO = {} #:nodoc + + # 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_SIZE = [0,0,0].pack(METRIC_PACK).bytesize #:nodoc: - - 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| + define_type :metrics, + :pack => lambda { |metrics,obj| metrics.map { |code,val| [METRIC_SIZE,code,val].pack(METRIC_PACK) }.join }, :unpack => lambda { |str,obj| res = {} # in kernel the dst.metrics structure is array of u32 RtattrMessage.unpack_rtattr(str) { |code,val| res[code] = val.unpack("L").first } res - }, - }, - :l2addr => { - :pack => lambda { |val,obj| Array(val).pack("H*") }, - :unpack => lambda { |val,obj| val.unpack("H*").first }, - }, - :l3addr => { + } + + # L2 addresses are presented as ASCII hex. You may optionally include + # colons, hyphens or dots. + # Link.new(:address => "00:11:22:33:44:55") # this is OK + define_type :l2addr, + :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| case obj.family when Socket::AF_INET, Socket::AF_INET6 @@ -91,10 +139,21 @@ module Netlink else IPAddr.new(val) 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 else - raise "Missing or mismatched address family" if val.is_a?(IPAddr) + raise "Mismatched address family" if val.is_a?(IPAddr) val end }, @@ -105,20 +164,8 @@ module Netlink else val 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) if h.instance_of?(self.class) @attrs = h.to_hash.dup @@ -152,13 +199,18 @@ module Netlink subclass.const_set(:DEFAULTS, {}) 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) codes.each { |code| CODE_TO_MESSAGE[code] = self } end # Define a field for this message, which creates accessor methods and # 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={}) info = TYPE_INFO[type] self::FIELDS << name @@ -173,7 +225,7 @@ module Netlink end # Returns the packed binary representation of this message (without - # header, and not padded to NLMSG_ALIGNTO bytes) + # header, and not padded to NLMSG_ALIGNTO bytes) def to_s self.class::FIELDS.map { |key| self[key] }.pack(self.class::FORMAT) end @@ -207,9 +259,11 @@ module Netlink end end - # This is a class for a Message which is followed by Rtattr key/value pairs. - # We assume that any particular attribute is not repeated, so it maps to - # a single attribute in the underlying hash. + # Extends Message to support variable Rtattr attributes. Use 'field' + # to define the fixed parts of the message, and 'rtattr' to define the + # 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 RTATTR_PACK = "S_S_".freeze #:nodoc: RTATTR_SIZE = [0,0].pack(RTATTR_PACK).bytesize #:nodoc: @@ -219,6 +273,10 @@ module Netlink subclass.const_set(:RTATTRS, {}) 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={}) info = TYPE_INFO[type] self::RTATTRS[code] = [name, info] @@ -230,15 +288,20 @@ module Netlink 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) 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 - data = super + data = "" self.class::RTATTRS.each do |code, (name, info)| if val = @attrs[name] - Message.pad(data) + Message.pad(data) # assume NLMSG_ALIGNTO == NLA_ALIGNTO if pack = info[:pack] val = pack[val,self] elsif pattern = info[:pattern] @@ -247,9 +310,12 @@ module Netlink data << [val.bytesize+RTATTR_SIZE, code].pack(RTATTR_PACK) << val end end - data + data.empty? ? super : Message.pad(super) + data 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) res = super attrs = res.to_hash @@ -273,6 +339,7 @@ module Netlink res end + # Unpack a string containing a sequence of rtattrs, yielding each in turn. def self.unpack_rtattr(data, ptr=0) #:nodoc: while ptr < 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 "Invalid rtattr len!" if 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 @@ -299,7 +366,7 @@ module Netlink rtattr :mtu, IFLA_MTU, :uint32 rtattr :link, IFLA_LINK, :int32 rtattr :qdisc, IFLA_QDISC, :cstring - rtattr :stats32, IFLA_STATS, :stats32 + rtattr :stats32, IFLA_STATS, :linkstats32 rtattr :cost, IFLA_COST rtattr :master, IFLA_MASTER, :uint32 rtattr :wireless, IFLA_WIRELESS @@ -314,7 +381,7 @@ module Netlink rtattr :ifalias, IFLA_IFALIAS, :cstring rtattr :num_vf, IFLA_NUM_VF, :uint32 rtattr :vfinfo_list, IFLA_VFINFO_LIST - rtattr :stats64, IFLA_STATS64, :stats64 + rtattr :stats64, IFLA_STATS64, :linkstats64 rtattr :vf_ports, IFLA_VF_PORTS rtattr :port_self, IFLA_PORT_SELF