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/ /spec/reports/
/tmp/ /tmp/
/Gemfile.lock
# ---> Vim # ---> Vim
# Swap # Swap
[._]*.s[a-v][a-z] [._]*.s[a-v][a-z]

View file

@ -1,4 +1,5 @@
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" spec.name = "knot-ruby"
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.3.0") spec.required_ruby_version = Gem::Requirement.new(">= 2.7.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,3 +2,13 @@ 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,22 +11,35 @@ 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
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 self.begin
yield self yield self
rescue Object rescue Object
@ -72,6 +85,7 @@ 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',
@ -113,20 +127,24 @@ class Knot::Conf
self.commit self.commit
end end
def parse_item k def parse_item kv
case k case kv
when k when Hash
case k.keys.sort r = {}
when %w[section], %w[id section], %w[item section], %w[id item section] then k kv.each {|k,v| r[k.to_s.to_sym] = v }
else raise ArgumentError, "Invalid Item-format" 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 end
when Array when Array
case k.length case kv.length
when 1 then {section: k[0]} when 1 then {section: kv[0]}
when 2 then {section: k[0], item: k[1]} when 2 then {section: kv[0], item: kv[1]}
when 3 then {section: k[0], id: k[1], item: k[2]} when 3 then {section: kv[0], id: kv[1], item: kv[2]}
else raise ArgumentError, "Invalid Item-format" else raise ArgumentError, "Invalid Item-format: #{kv}"
end end
when /\A when /\A
@ -134,26 +152,39 @@ class Knot::Conf
(?: \[ (?<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 (item ? parse_item( item) : {}).update( command: 'conf-list') @protocol.call **parse_item( item).update( command: 'conf-list')
end end
def read item = nil 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
end end

View file

@ -1,4 +1,7 @@
require 'iounpack' require 'iounpack'
require 'pathname'
require 'socket'
require 'logger'
require_relative 'errors' require_relative 'errors'
module Knot module Knot
@ -51,44 +54,120 @@ module Knot::Protocol::Type
end 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 module Knot::Protocol::Idx
Idx = [ extend Knot::Protocol::Codes
:command, # 10, :CMD, # Control command name.
:flags, # 11, :FLAGS, # Control command flags. Codes = [
:error, # 12, :ERROR, # Error message. Knot::Protocol::Code.new( :command, 0x10, :CMD, 'Control command name.'),
:section, # 13, :SECTION, # Configuration section name. Knot::Protocol::Code.new( :flags, 0x11, :FLAGS, 'Control command flags.'),
:item, # 14, :ITEM, # Configuration item name. Knot::Protocol::Code.new( :error, 0x12, :ERROR, 'Error message.'),
:id, # 15, :ID, # Congiguration item identifier. Knot::Protocol::Code.new( :section, 0x13, :SECTION, 'Configuration section name.'),
:zone, # 16, :ZONE, # Zone name. Knot::Protocol::Code.new( :item, 0x14, :ITEM, 'Configuration item name.'),
:owner, # 17, :OWNER, # Zone record owner Knot::Protocol::Code.new( :id, 0x15, :ID, 'Congiguration item identifier.'),
:ttl, # 18, :TTL, # Zone record TTL. Knot::Protocol::Code.new( :zone, 0x16, :ZONE, 'Zone name.'),
:type, # 19, :TYPE, # Zone record type name. Knot::Protocol::Code.new( :owner, 0x17, :OWNER, 'Zone record owner'),
:data, # 1a, :DATA, # Configuration item/zone record data. Knot::Protocol::Code.new( :ttl, 0x18, :TTL, 'Zone record TTL.'),
:filter, # 1b, :FILTER, # An option or a filter for output data processing. 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 = {} Name = {}
Code = {} Code = {}
Idx.each_with_index do |v, i|
Code[0x10+i] = v Codes.each do |id|
Name[v] = i Code[id.to_i] = id
end Name[id.to_sym] = id
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
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 end
class Knot::Protocol class Knot::Protocol
attr_reader :sock, :conf, :zones attr_reader :sock, :conf, :zones, :logger
attr_accessor :debug
def initialize path_or_sock = nil def initialize path_or_sock = nil, logger: 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
@ -97,24 +176,24 @@ class Knot::Protocol
when nil when nil
@sock = UNIXSocket.new '/run/knot/knot.sock' @sock = UNIXSocket.new '/run/knot/knot.sock'
end end
@debug = false @logger = logger || Logger.new(STDERR)
@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
s = '' data = Hash[ *data.map {|k,v| [k.to_sym, v]}.flatten]
sock = StringIO.new s
sock.write [1].pack( 'c')
data[:flags] ||= '' data[:flags] ||= ''
Idx::Idx.each_with_index do |n, i| ds =
v = data[n]&.to_s Idx.
sock.write [0x10+i, v.size, v].pack( 'c na*') if v 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 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
@ -123,45 +202,48 @@ class Knot::Protocol
attr_reader :str attr_reader :str
def initialize sock, str = nil def initialize sock, str = nil
@str, @sock = str || '', sock @str, @sock = str || ''.b, 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 @str.insert -1, s unless s.nil?
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 @debug sock = RecordIO.new sock if 0 >= @logger.sev_threshold
loop do loop do
t = sock.unpack1 'c' t = sock.unpack1 'c'
case t case t
when 0, 3 when Knot::Protocol::Types[:end], Knot::Protocol::Types[:block]
return ret return ret
when 1, 2 when Knot::Protocol::Types[:data], Knot::Protocol::Types[:extra]
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::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' l = sock.unpack1 'n'
r[i] = sock.read( l) r[i.to_sym] = sock.read( l)
end end
end end
ensure 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 ret
end end
@ -179,10 +261,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-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_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.1.0" VERSION = "0.3.2"
end end