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
Name[id.to_sym] = id
end 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
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,7 +202,7 @@ 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
@ -136,32 +215,35 @@ class Knot::Protocol
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