Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

7 changed files with 82 additions and 208 deletions

2
.gitignore vendored
View file

@ -8,8 +8,6 @@
/spec/reports/ /spec/reports/
/tmp/ /tmp/
/Gemfile.lock
# ---> Vim # ---> Vim
# Swap # Swap
[._]*.s[a-v][a-z] [._]*.s[a-v][a-z]

View file

@ -1,5 +1,4 @@
source "https://rubygems.org" source "https://rubygems.org"
ruby '>=2.7'
# Specify your gem's dependencies in knot.gemspec # Specify your gem's dependencies in knot.gemspec
gemspec gemspec

View file

@ -1,7 +1,7 @@
require_relative 'lib/knot/version' require_relative 'lib/knot/version'
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "knot-ruby" spec.name = "knot"
spec.version = Knot::VERSION spec.version = Knot::VERSION
spec.authors = ['Denis Knauf'] spec.authors = ['Denis Knauf']
spec.email = ['gems+knot@denkn.at'] spec.email = ['gems+knot@denkn.at']
@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
spec.summary = %q{Provides interface to knot-server.} spec.summary = %q{Provides interface to knot-server.}
spec.description = %q{Implements knot-protocol to provide an interface to knot-DNS-server} spec.description = %q{Implements knot-protocol to provide an interface to knot-DNS-server}
spec.homepage = 'https://git.denkn.at/deac/knot' spec.homepage = 'https://git.denkn.at/deac/knot'
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" #spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

View file

@ -2,13 +2,3 @@ require 'knot/version'
require 'knot/errors' require 'knot/errors'
require 'knot/protocol' require 'knot/protocol'
require 'knot/interface' require 'knot/interface'
module Knot
class <<self
def new *as, **os
Protocol.new *as, **os
end
alias connect new
alias open new
end
end

View file

@ -11,35 +11,22 @@ class Knot::Zone
@zone, @transaction_opened = zone, 0 @zone, @transaction_opened = zone, 0
end end
# If no transaction opened, yet, it opens a new transaction.
# Counts calling begin.
# Knot allowes only one global transaction, so if two clients tries to open a transaction,
# the second will fail. But the second client can change items in the same
# transaction like the first client.
# The first client, which calls commit or abort, wins and the transaction will be closed.
# So, the transaction-handling is only safe, if one client connects to knot.
def begin def begin
@transaction_opened += 1 @transaction_opened += 1
@protocol.call command: 'zone-begin', zone: @zone if 1 == @transaction_opened @protocol.call command: 'zone-begin', zone: @zone if 1 == @transaction_opened
end end
# Decreases opened transactions.
# If opened transactions is zero, it stores items and closes transaction successfully.
def commit def commit
@protocol.call command: 'zone-commit', zone: @zone if 1 == @transaction_opened @protocol.call command: 'zone-commit', zone: @zone if 1 == @transaction_opened
@transaction_opened -= 1 if 0 < @transaction_opened @transaction_opened -= 1 if 0 < @transaction_opened
end end
# Closes transaction without saving immidiatly.
# Counter of opened transaction will be reset to 0.
def abort def abort
@protocol.call command: 'zone-abort', zone: @zone @protocol.call command: 'zone-abort', zone: @zone
@transaction_opened = 0 @transaction_opened = 0
end end
# Opens transaction, calls yielded Proc and closes transaction after return. def transaction
# If exception was raised, the transaction will be aborted.
def transaction # :yield:
self.begin self.begin
yield self yield self
rescue Object rescue Object
@ -69,7 +56,7 @@ class Knot::Zone
def read() @protocol.call command: 'zone-read', zone: @zone end def read() @protocol.call command: 'zone-read', zone: @zone end
def diff() @protocol.call command: 'zone-diff', zone: @zone end def diff() @protocol.call command: 'zone-diff', zone: @zone end
# setting record # setting record
# if data is nil, it will be unset. # if data is nil, it will be unset.
def set owner, ttl = nil, type, data def set owner, ttl = nil, type, data
@ -85,7 +72,6 @@ class Knot::Zone
rescue Knot::Errors::ENONODE, Knot::Errors::ENOENT rescue Knot::Errors::ENONODE, Knot::Errors::ENOENT
end end
alias delete unset alias delete unset
alias del unset
def get owner = nil, type = nil def get owner = nil, type = nil
@protocol.call command: 'zone-get', @protocol.call command: 'zone-get',
@ -127,64 +113,47 @@ class Knot::Conf
self.commit self.commit
end end
def parse_item kv def parse_item k
case kv case k
when Hash when k
r = {} case k.keys.sort
kv.each {|k,v| r[k.to_s.to_sym] = v } when %w[section], %w[id section], %w[item section], %w[id item section] then k
case r.keys.sort else raise ArgumentError, "Invalid Item-format"
when %i[section], %i[id section], %i[item section], %i[id item section]
r
else
raise ArgumentError, "Invalid Item-format: #{k}"
end end
when Array when Array
case kv.length case k.length
when 1 then {section: kv[0]} when 1 then {section: k[0]}
when 2 then {section: kv[0], item: kv[1]} when 2 then {section: k[0], item: k[1]}
when 3 then {section: kv[0], id: kv[1], item: kv[2]} when 3 then {section: k[0], id: k[1], item: k[2]}
else raise ArgumentError, "Invalid Item-format: #{kv}" else raise ArgumentError, "Invalid Item-format"
end end
when /\A when /\A
(?<section> [a-z0-9_-]+ ) (?<section> [a-z0-9_-]+ )
(?: \[ (?<id> [a-z0-9_.-]+) \] )? (?: \[ (?<id> [a-z0-9_.-]+) \] )?
(?: \. (?<item>[a-z0-9_-]+) )? (?: \. (?<item>[a-z0-9_-]+) )?
\z/xi \z/xi
$~.named_captures.delete_if {|_,v| v.nil? }
$~.named_captures.delete_if {|_,v| v.nil? } else raise ArgumentError, "Invalid Item-format"
when nil
{}
else
raise ArgumentError, "Invalid Item-format: #{kv}"
end end
end end
def get item = nil
@protocol.call **parse_item( item).update( command: 'conf-get')
end
# Sets or adds a new value to item.
# knot knows single-value items like `server.rundir` and multi-value items like `server.listen`.
# If you set a single-value, it will replace the old value. On a multi-value, it will add it.
def set item, value def set item, value
@protocol.call **parse_item( item).update( command: 'conf-set', data: value) @protocol.call parse_item( item).update( command: 'conf-set', data: value)
end end
alias [] set
# Removes value from item. If you provide a value, this value will be removed.
def unset item, value = nil def unset item, value = nil
@protocol.call **parse_item( item).update( command: 'conf-unset', data: value) @protocol.call parse_item( item).update( command: 'conf-unset', data: value)
end end
alias delete unset alias delete unset
def list item = nil def list item = nil
@protocol.call **parse_item( item).update( command: 'conf-list') @protocol.call (item ? parse_item( item) : {}).update( command: 'conf-list')
end end
def read item = nil def read item = nil
@protocol.call **parse_item( item).update( command: 'conf-read') @protocol.call (item ? parse_item( item) : {}).update( command: 'conf-read')
end end
end end

View file

@ -1,7 +1,4 @@
require 'iounpack' require 'iounpack'
require 'pathname'
require 'socket'
require 'logger'
require_relative 'errors' require_relative 'errors'
module Knot module Knot
@ -54,120 +51,44 @@ module Knot::Protocol::Type
end end
end end
#class Knot::KnotC module Knot::Protocol::Idx
# attr_accessor :binary Idx = [
# :command, # 10, :CMD, # Control command name.
# def initialize path = nil, binary: nil :flags, # 11, :FLAGS, # Control command flags.
# @path = path :error, # 12, :ERROR, # Error message.
# @binary = binary || 'knotc' :section, # 13, :SECTION, # Configuration section name.
# @conf = Knot::Conf.new self :item, # 14, :ITEM, # Configuration item name.
# @zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self } :id, # 15, :ID, # Congiguration item identifier.
# end :zone, # 16, :ZONE, # Zone name.
# :owner, # 17, :OWNER, # Zone record owner
# def call command:, flags: nil, section: nil, item: nil, id: nil, zone: nil, owner: nil, ttl: nil, type: nil, data: nil, filter: nil :ttl, # 18, :TTL, # Zone record TTL.
# cs = :type, # 19, :TYPE, # Zone record type name.
# case command.to_s :data, # 1a, :DATA, # Configuration item/zone record data.
# when 'conf-begin', 'conf-commit', 'conf-abort', 'status', 'stop', 'reload' :filter, # 1b, :FILTER, # An option or a filter for output data processing.
# [@binary, command, ] ]
# else raise ArgumentError, "Unknown Command: #{command}" Name = {}
# end Code = {}
# end Idx.each_with_index do |v, i|
#end Code[0x10+i] = v
Name[v] = i
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 end
def self.[] k
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 case k
when Symbol when Symbol
self::Name[k] or raise Knot::Errors::EINVAL, "Unknown Codes: #{k}" Name[k] or raise Knot::Errors::EINVAL, "Unknown Idx: #{k}"
when Integer when Integer
self::Code[k] or raise Knot::Errors::EINVAL, "Unknown Codes: #{k}" Idx[k] or raise Knot::Errors::EINVAL, "Unknown Idx: #{k}"
else else
raise ArgumentError, "Unknown Codes-Type: #{k}" raise ArgumentError, "Unknown Idx-Type: #{k}"
end end
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 end
class Knot::Protocol class Knot::Protocol
attr_reader :sock, :conf, :zones, :logger attr_reader :sock, :conf, :zones
attr_accessor :debug
def initialize path_or_sock = nil, logger: nil def initialize path_or_sock = nil
case path_or_sock case path_or_sock
when String, Pathname when String, Pathname
@sock = UNIXSocket.new path_or_sock.to_s @sock = UNIXSocket.new path_or_sock.to_s
@ -176,80 +97,77 @@ class Knot::Protocol
when nil when nil
@sock = UNIXSocket.new '/run/knot/knot.sock' @sock = UNIXSocket.new '/run/knot/knot.sock'
end end
@logger = logger || Logger.new(STDERR) @debug = false
@conf = Knot::Conf.new self @conf = Knot::Conf.new self
@zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self } @zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self }
end end
def snd sock: nil, **data def snd sock: nil, **data
rsock = sock || @sock rsock = sock || @sock
data = Hash[ *data.map {|k,v| [k.to_sym, v]}.flatten] s = ''
sock = StringIO.new s
sock.write [1].pack( 'c')
data[:flags] ||= '' data[:flags] ||= ''
ds = Idx::Idx.each_with_index do |n, i|
Idx. v = data[n]&.to_s
select {|n| data[n.to_sym] }. sock.write [0x10+i, v.size, v].pack( 'c na*') if v
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 end
sock.write [3].pack( 'c')
sock.flush
STDERR.puts( {data: data, _: s}.inspect) if @debug
rsock.write s rsock.write s
rsock.flush rsock.flush
end end
class RecordIO class RecordIO
attr_reader :str attr_reader :str
def initialize sock, str = nil def initialize sock, str = nil
@str, @sock = str || ''.b, sock @str, @sock = str || '', sock
end end
def unpack pattern def unpack pattern
IOUnpack.new( pattern).unpack self IOUnpack.new(pattern).unpack self
end end
def unpack1 pattern def unpack1 pattern
IOUnpack.new( pattern).unpack1 self IOUnpack.new(pattern).unpack1 self
end end
def read n def read n
s = @sock.read n s = @sock.read n
@str.insert -1, s unless s.nil? @str.insert -1, s
s || '' s
end end
end end
def rcv sock: nil def rcv sock: nil
ret, r = [], nil ret, r = [], nil
sock = sock || @sock sock = sock || @sock
sock = RecordIO.new sock if 0 >= @logger.sev_threshold sock = RecordIO.new sock if @debug
loop do loop do
t = sock.unpack1 'c' t = sock.unpack1 'c'
case t case t
when Knot::Protocol::Types[:end], Knot::Protocol::Types[:block] when 0, 3
return ret return ret
when Knot::Protocol::Types[:data], Knot::Protocol::Types[:extra] when 1, 2
type = t type = t
ret.push( r = {}) ret.push( r = {})
else else
raise Knot::Errors::EINVAL, "Missing Type before: #{t}" if ret.empty? raise Knot::Errors::EINVAL, "Missing Type before: #{t}" if ret.empty?
i = Idx[t] or raise Knot::Errors::EINVAL, "Unknown index: #{t}" i = Idx::Idx[t - 0x10] or raise Knot::Errors::EINVAL, "Unknown index: #{t-0x10}"
l = sock.unpack1 'n' l = sock.unpack1 'n'
r[i.to_sym] = sock.read( l) r[i] = sock.read( l)
end end
end end
ensure ensure
if RecordIO === sock STDERR.puts( {rcvd: ret, read: sock.str}.inspect) if @debug
@logger.debug "rcvd raw #{sock.str.inspect}"
@logger.debug "rcvd data #{ret.inspect}"
end
ret ret
end end
def call sock: nil, **data def call sock: nil, **data
snd sock: sock, **data snd sock: sock, **data
rcv( sock: sock).each do |r| rcv( sock: sock).each do |r|
if r[:error] if r[:error]
if e = Knot::Errors.err2exc[r[:error]] if e = Knot::Errors.err2exc[r[:error]]
raise e, r[:error] raise e, r[:error]
@ -261,10 +179,10 @@ class Knot::Protocol
def zone( zone) @zones[zone.to_s.to_sym] end def zone( zone) @zones[zone.to_s.to_sym] end
def conf_set( **opts) call **opts.update( command: 'conf-set') end def conf_set( **opts) call opts.update( command: 'conf-set') end
def conf_unset( **opts) call **opts.update( command: 'conf-unset') end def conf_unset( **opts) call opts.update( command: 'conf-set') end
def zone_set( **opts) call **opts.update( command: 'zone-set') 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_unset( **opts) call opts.update( command: 'zone-unset') end
def zone_get( **opts) call **opts.update( command: 'zone-get') end def zone_get( **opts) call opts.update( command: 'zone-get') end
end end

View file

@ -1,3 +1,3 @@
module Knot module Knot
VERSION = "0.3.2" VERSION = "0.1.0"
end end