knot-ruby/lib/knot/protocol.rb

271 lines
6.7 KiB
Ruby

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