Compare commits

...

17 Commits

Author SHA1 Message Date
Denis Knauf b21f7c319d v0.3.2 (debugging removed) 2022-12-13 23:43:18 +01:00
Denis Knauf 95cabef567 v0.3.1 2022-12-13 23:41:16 +01:00
Denis Knauf 12639f766c Knot::Protocol#snd supports strings as key, too (calls to_sym) 2022-12-13 23:38:43 +01:00
Denis Knauf 59db51d2ed v0.3.0 2022-12-13 22:45:39 +01:00
Denis Knauf e9321d4de6 Protocol-Types like Idx. Codes for sharing code. 2022-12-13 22:44:06 +01:00
Denis Knauf 2255c784bc v0.2.1 2022-12-13 20:48:04 +01:00
Denis Knauf 7e02cbbe84 Protocol#snd: StringIO eliminated, more functional. 2022-09-29 11:52:51 +02:00
Denis Knauf 18c0b6434a Knot::Protocol#debug replaced by #logger, which could be provided by #new(logger:), default Logger.new(STDERR) 2022-09-28 12:41:52 +02:00
Denis Knauf 0282694a28 Knot::Protocol: Fix iteration. 2022-09-28 11:06:53 +02:00
Denis Knauf 88e38eab7a Knot::Protocol::Idx fix: if -> unless 2022-09-28 11:04:35 +02:00
Denis Knauf 10645196d1 binary strings encoded binary. Idx::Id instead of Symbol in Idx. parse_items only allowes Symbols as options. 2022-09-28 11:01:29 +02:00
Denis Knauf 6af5922217 Version 0.2.0 - required ruby>=2.7. **-kw-options. BETA 2022-09-27 20:58:19 +02:00
Denis Knauf 0a0392d532 0.1.2 2022-06-17 12:47:37 +02:00
Denis Knauf d25e1a9762 ruby-2.7 needs ** for key-value-pair-arguments 2022-06-17 12:46:37 +02:00
Denis Knauf ad44237905 v0.1.1 2020-12-02 16:38:59 +01:00
Denis Knauf d51b56a67b Comments for Conf-interface. get-method added. 2020-12-02 16:20:58 +01:00
Denis Knauf 0af010b358 spec name knot-ruby. ignore Gemfile.lock 2020-06-20 23:02:54 +02:00
7 changed files with 210 additions and 84 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -2,3 +2,13 @@ require 'knot/version'
require 'knot/errors'
require 'knot/protocol'
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,22 +11,35 @@ class Knot::Zone
@zone, @transaction_opened = zone, 0
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
@transaction_opened += 1
@protocol.call command: 'zone-begin', zone: @zone if 1 == @transaction_opened
end
# Decreases opened transactions.
# If opened transactions is zero, it stores items and closes transaction successfully.
def commit
@protocol.call command: 'zone-commit', zone: @zone if 1 == @transaction_opened
@transaction_opened -= 1 if 0 < @transaction_opened
end
# Closes transaction without saving immidiatly.
# Counter of opened transaction will be reset to 0.
def abort
@protocol.call command: 'zone-abort', zone: @zone
@transaction_opened = 0
end
def transaction
# Opens transaction, calls yielded Proc and closes transaction after return.
# If exception was raised, the transaction will be aborted.
def transaction # :yield:
self.begin
yield self
rescue Object
@ -56,7 +69,7 @@ class Knot::Zone
def read() @protocol.call command: 'zone-read', zone: @zone end
def diff() @protocol.call command: 'zone-diff', zone: @zone end
# setting record
# if data is nil, it will be unset.
def set owner, ttl = nil, type, data
@ -72,6 +85,7 @@ class Knot::Zone
rescue Knot::Errors::ENONODE, Knot::Errors::ENOENT
end
alias delete unset
alias del unset
def get owner = nil, type = nil
@protocol.call command: 'zone-get',
@ -113,47 +127,64 @@ class Knot::Conf
self.commit
end
def parse_item k
case k
when k
case k.keys.sort
when %w[section], %w[id section], %w[item section], %w[id item section] then k
else raise ArgumentError, "Invalid Item-format"
def parse_item kv
case kv
when Hash
r = {}
kv.each {|k,v| r[k.to_s.to_sym] = v }
case r.keys.sort
when %i[section], %i[id section], %i[item section], %i[id item section]
r
else
raise ArgumentError, "Invalid Item-format: #{k}"
end
when Array
case k.length
when 1 then {section: k[0]}
when 2 then {section: k[0], item: k[1]}
when 3 then {section: k[0], id: k[1], item: k[2]}
else raise ArgumentError, "Invalid Item-format"
case kv.length
when 1 then {section: kv[0]}
when 2 then {section: kv[0], item: kv[1]}
when 3 then {section: kv[0], id: kv[1], item: kv[2]}
else raise ArgumentError, "Invalid Item-format: #{kv}"
end
when /\A
(?<section> [a-z0-9_-]+ )
(?: \[ (?<id> [a-z0-9_.-]+) \] )?
(?: \. (?<item>[a-z0-9_-]+) )?
(?<section> [a-z0-9_-]+ )
(?: \[ (?<id> [a-z0-9_.-]+) \] )?
(?: \. (?<item>[a-z0-9_-]+) )?
\z/xi
$~.named_captures.delete_if {|_,v| v.nil? }
else raise ArgumentError, "Invalid Item-format"
$~.named_captures.delete_if {|_,v| v.nil? }
when nil
{}
else
raise ArgumentError, "Invalid Item-format: #{kv}"
end
end
def set item, value
@protocol.call parse_item( item).update( command: 'conf-set', data: value)
def get item = nil
@protocol.call **parse_item( item).update( command: 'conf-get')
end
alias [] set
# 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
@protocol.call **parse_item( item).update( command: 'conf-set', data: value)
end
# Removes value from item. If you provide a value, this value will be removed.
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
alias delete unset
def list item = nil
@protocol.call (item ? parse_item( item) : {}).update( command: 'conf-list')
@protocol.call **parse_item( item).update( command: 'conf-list')
end
def read item = nil
@protocol.call (item ? parse_item( item) : {}).update( command: 'conf-read')
@protocol.call **parse_item( item).update( command: 'conf-read')
end
end

View File

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

View File

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