Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,8 +8,6 @@
|
|||
/spec/reports/
|
||||
/tmp/
|
||||
|
||||
/Gemfile.lock
|
||||
|
||||
# ---> Vim
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -1,5 +1,4 @@
|
|||
source "https://rubygems.org"
|
||||
ruby '>=2.7'
|
||||
|
||||
# Specify your gem's dependencies in knot.gemspec
|
||||
gemspec
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require_relative 'lib/knot/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "knot-ruby"
|
||||
spec.name = "knot"
|
||||
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.7.0")
|
||||
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
||||
|
||||
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
||||
|
||||
|
|
10
lib/knot.rb
10
lib/knot.rb
|
@ -2,13 +2,3 @@ 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
|
||||
|
|
|
@ -11,35 +11,22 @@ 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
|
||||
|
||||
# Opens transaction, calls yielded Proc and closes transaction after return.
|
||||
# If exception was raised, the transaction will be aborted.
|
||||
def transaction # :yield:
|
||||
def transaction
|
||||
self.begin
|
||||
yield self
|
||||
rescue Object
|
||||
|
@ -69,7 +56,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
|
||||
|
@ -85,7 +72,6 @@ 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',
|
||||
|
@ -127,64 +113,47 @@ class Knot::Conf
|
|||
self.commit
|
||||
end
|
||||
|
||||
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}"
|
||||
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"
|
||||
end
|
||||
|
||||
when Array
|
||||
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}"
|
||||
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"
|
||||
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? }
|
||||
|
||||
when nil
|
||||
{}
|
||||
|
||||
else
|
||||
raise ArgumentError, "Invalid Item-format: #{kv}"
|
||||
$~.named_captures.delete_if {|_,v| v.nil? }
|
||||
else raise ArgumentError, "Invalid Item-format"
|
||||
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
|
||||
@protocol.call **parse_item( item).update( command: 'conf-set', data: value)
|
||||
@protocol.call parse_item( item).update( command: 'conf-set', data: value)
|
||||
end
|
||||
alias [] set
|
||||
|
||||
# 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 **parse_item( item).update( command: 'conf-list')
|
||||
@protocol.call (item ? parse_item( item) : {}).update( command: 'conf-list')
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
require 'iounpack'
|
||||
require 'pathname'
|
||||
require 'socket'
|
||||
require 'logger'
|
||||
require_relative 'errors'
|
||||
|
||||
module Knot
|
||||
|
@ -54,120 +51,44 @@ 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
|
||||
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.
|
||||
]
|
||||
Name = {}
|
||||
Code = {}
|
||||
Idx.each_with_index do |v, i|
|
||||
Code[0x10+i] = v
|
||||
Name[v] = i
|
||||
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
|
||||
def self.[] k
|
||||
case k
|
||||
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
|
||||
self::Code[k] or raise Knot::Errors::EINVAL, "Unknown Codes: #{k}"
|
||||
Idx[k] or raise Knot::Errors::EINVAL, "Unknown Idx: #{k}"
|
||||
else
|
||||
raise ArgumentError, "Unknown Codes-Type: #{k}"
|
||||
raise ArgumentError, "Unknown Idx-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
|
||||
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
|
||||
when String, Pathname
|
||||
@sock = UNIXSocket.new path_or_sock.to_s
|
||||
|
@ -176,80 +97,77 @@ class Knot::Protocol
|
|||
when nil
|
||||
@sock = UNIXSocket.new '/run/knot/knot.sock'
|
||||
end
|
||||
@logger = logger || Logger.new(STDERR)
|
||||
@debug = false
|
||||
@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]
|
||||
s = ''
|
||||
sock = StringIO.new s
|
||||
sock.write [1].pack( 'c')
|
||||
data[:flags] ||= ''
|
||||
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}"
|
||||
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
|
||||
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 || ''.b, sock
|
||||
@str, @sock = str || '', 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 unless s.nil?
|
||||
s || ''
|
||||
@str.insert -1, s
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
def rcv sock: nil
|
||||
ret, r = [], nil
|
||||
sock = sock || @sock
|
||||
sock = RecordIO.new sock if 0 >= @logger.sev_threshold
|
||||
sock = RecordIO.new sock if @debug
|
||||
loop do
|
||||
t = sock.unpack1 'c'
|
||||
case t
|
||||
when Knot::Protocol::Types[:end], Knot::Protocol::Types[:block]
|
||||
when 0, 3
|
||||
return ret
|
||||
when Knot::Protocol::Types[:data], Knot::Protocol::Types[:extra]
|
||||
when 1, 2
|
||||
type = t
|
||||
ret.push( r = {})
|
||||
else
|
||||
else
|
||||
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'
|
||||
r[i.to_sym] = sock.read( l)
|
||||
r[i] = sock.read( l)
|
||||
end
|
||||
end
|
||||
ensure
|
||||
if RecordIO === sock
|
||||
@logger.debug "rcvd raw #{sock.str.inspect}"
|
||||
@logger.debug "rcvd data #{ret.inspect}"
|
||||
end
|
||||
STDERR.puts( {rcvd: ret, read: sock.str}.inspect) if @debug
|
||||
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]
|
||||
|
@ -261,10 +179,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-unset') end
|
||||
def conf_set( **opts) call opts.update( command: 'conf-set') 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_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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module Knot
|
||||
VERSION = "0.3.2"
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue