require 'iounpack' require 'pathname' require 'socket' require 'logger' require_relative 'errors' module Knot class Protocol end end module Knot::Protocol::Type def [] code case code when 0, End then return End when 1, Data then return Data when 2, Extra then return Extra when 3, Block then return Block end end class Base def self.expect_data? false end def expect_data?() self.class.expect_data? end def code() self.class.code end end class End < Base def self.code() 0 end end class Data < Base def code() 1 end def expect_data? true end attr_reader :data def initialize data @data = data end end class Extra < Base def code() 2 end def expect_data? true end attr_reader :data def initialize data @data = data end end class Block < Base def code() 3 end end end #class Knot::KnotC # attr_accessor :binary # # def initialize path = nil, binary: nil # @path = path # @binary = binary || 'knotc' # @conf = Knot::Conf.new self # @zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self } # end # # def call command:, flags: nil, section: nil, item: nil, id: nil, zone: nil, owner: nil, ttl: nil, type: nil, data: nil, filter: nil # cs = # case command.to_s # when 'conf-begin', 'conf-commit', 'conf-abort', 'status', 'stop', 'reload' # [@binary, command, ] # else raise ArgumentError, "Unknown Command: #{command}" # end # end #end class Knot::Protocol::Code include Comparable attr_reader :name, :code, :cname, :description def initialize name, code, cname, description raise ArgumentError, "Expecting Symbol for #{self.class.name} instead of: #{name.inspect}" unless Symbol === name raise ArgumentError, "Expecting Integer for #{self.class.name} instead of: #{code.inspect}" unless Integer === code @name, @code, @cname, @description = name, code, cname, description freeze end def === x case x when self.class then @id == x.id when Symbol then @name == x when String then @name == x.to_sym when Integer then @code == x else nil end end def <=>( x) @id <=> x.id end def to_s() @name.to_s end def to_sym() @name end def to_i() @code end end module Knot::Protocol::Codes include Enumerable def [] k case k when Symbol self::Name[k] or raise Knot::Errors::EINVAL, "Unknown Codes: #{k}" when Integer self::Code[k] or raise Knot::Errors::EINVAL, "Unknown Codes: #{k}" else raise ArgumentError, "Unknown Codes-Type: #{k}" end end def each &exe block_given? ? self::Codes.each( &exe) : self::Codes.to_enum( :each) end end module Knot::Protocol::Idx extend Knot::Protocol::Codes Codes = [ Knot::Protocol::Code.new( :command, 0x10, :CMD, 'Control command name.'), Knot::Protocol::Code.new( :flags, 0x11, :FLAGS, 'Control command flags.'), Knot::Protocol::Code.new( :error, 0x12, :ERROR, 'Error message.'), Knot::Protocol::Code.new( :section, 0x13, :SECTION, 'Configuration section name.'), Knot::Protocol::Code.new( :item, 0x14, :ITEM, 'Configuration item name.'), Knot::Protocol::Code.new( :id, 0x15, :ID, 'Congiguration item identifier.'), Knot::Protocol::Code.new( :zone, 0x16, :ZONE, 'Zone name.'), Knot::Protocol::Code.new( :owner, 0x17, :OWNER, 'Zone record owner'), Knot::Protocol::Code.new( :ttl, 0x18, :TTL, 'Zone record TTL.'), Knot::Protocol::Code.new( :type, 0x19, :TYPE, 'Zone record type name.'), Knot::Protocol::Code.new( :data, 0x1a, :DATA, 'Configuration item/zone record data.'), Knot::Protocol::Code.new( :filter, 0x1b, :FILTER, 'An option or a filter for output data processing.'), ] Name = {} Code = {} Codes.each do |id| Code[id.to_i] = id Name[id.to_sym] = id end end module Knot::Protocol::Types extend Knot::Protocol::Codes Codes = [ Knot::Protocol::Code.new( :end, 0x00, :END, 'Type END.'), Knot::Protocol::Code.new( :data, 0x01, :DATA, 'Type DATA.'), Knot::Protocol::Code.new( :extra, 0x02, :EXTRA, 'Type EXTRA.'), Knot::Protocol::Code.new( :block, 0x03, :BLOCK, 'Type BLOCK.'), ] Name = {} Code = {} Codes.each do |id| Code[id.to_i] = id Name[id.to_sym] = id end end class Knot::Protocol attr_reader :sock, :conf, :zones, :logger def initialize path_or_sock = nil, logger: nil case path_or_sock when String, Pathname @sock = UNIXSocket.new path_or_sock.to_s when Socket @sock = path_or_sock when nil @sock = UNIXSocket.new '/run/knot/knot.sock' end @logger = logger || Logger.new(STDERR) @conf = Knot::Conf.new self @zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self } end def snd sock: nil, **data rsock = sock || @sock data = Hash[ *data.map {|k,v| [k.to_sym, v]}.flatten] data[:flags] ||= '' ds = Idx. select {|n| p [n.to_sym, data[n.to_sym]]; data[n.to_sym] }. map {|n| v = data[n.to_sym].to_s.b; [n.to_i, v.size, v] } s = [Types[:data].to_i, ds, Types[:block].to_i].flatten.pack( "c #{'c na*'*ds.length} c").b if 0 >= @logger.sev_threshold @logger.debug "send data #{data.inspect}" @logger.debug "send raw #{s.inspect}" end rsock.write s rsock.flush end class RecordIO attr_reader :str def initialize sock, str = nil @str, @sock = str || ''.b, sock end def unpack pattern IOUnpack.new( pattern).unpack self end def unpack1 pattern IOUnpack.new( pattern).unpack1 self end def read n s = @sock.read n @str.insert -1, s unless s.nil? s || '' end end def rcv sock: nil ret, r = [], nil sock = sock || @sock sock = RecordIO.new sock if 0 >= @logger.sev_threshold loop do t = sock.unpack1 'c' case t when Knot::Protocol::Types[:end], Knot::Protocol::Types[:block] return ret when Knot::Protocol::Types[:data], Knot::Protocol::Types[:extra] type = t ret.push( r = {}) else raise Knot::Errors::EINVAL, "Missing Type before: #{t}" if ret.empty? i = Idx[t] or raise Knot::Errors::EINVAL, "Unknown index: #{t}" l = sock.unpack1 'n' r[i.to_sym] = sock.read( l) end end ensure if RecordIO === sock @logger.debug "rcvd raw #{sock.str.inspect}" @logger.debug "rcvd data #{ret.inspect}" end ret end def call sock: nil, **data snd sock: sock, **data rcv( sock: sock).each do |r| if r[:error] if e = Knot::Errors.err2exc[r[:error]] raise e, r[:error] end raise Knot::Error, r[:error] end end end def zone( zone) @zones[zone.to_s.to_sym] end def conf_set( **opts) call **opts.update( command: 'conf-set') end def conf_unset( **opts) call **opts.update( command: 'conf-unset') end def zone_set( **opts) call **opts.update( command: 'zone-set') end def zone_unset( **opts) call **opts.update( command: 'zone-unset') end def zone_get( **opts) call **opts.update( command: 'zone-get') end end