diff --git a/.gitignore b/.gitignore index f5587e6..3e7b48d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ /spec/reports/ /tmp/ +/Gemfile.lock + # ---> Vim # Swap [._]*.s[a-v][a-z] diff --git a/Gemfile b/Gemfile index 548b363..844a1ab 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ source "https://rubygems.org" +ruby '>=2.7' # Specify your gem's dependencies in knot.gemspec gemspec diff --git a/knot-ruby.gemspec b/knot-ruby.gemspec index 9076bd2..d8dded8 100644 --- a/knot-ruby.gemspec +++ b/knot-ruby.gemspec @@ -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'" diff --git a/lib/knot.rb b/lib/knot.rb index 4231108..32f0701 100644 --- a/lib/knot.rb +++ b/lib/knot.rb @@ -2,3 +2,13 @@ require 'knot/version' require 'knot/errors' require 'knot/protocol' require 'knot/interface' + +module Knot + class < [a-z0-9_-]+ ) - (?: \[ (? [a-z0-9_.-]+) \] )? - (?: \. (?[a-z0-9_-]+) )? + (?
[a-z0-9_-]+ ) + (?: \[ (? [a-z0-9_.-]+) \] )? + (?: \. (?[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 diff --git a/lib/knot/protocol.rb b/lib/knot/protocol.rb index 903697b..e05a112 100644 --- a/lib/knot/protocol.rb +++ b/lib/knot/protocol.rb @@ -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 diff --git a/lib/knot/version.rb b/lib/knot/version.rb index 4db3f7f..46de30f 100644 --- a/lib/knot/version.rb +++ b/lib/knot/version.rb @@ -1,3 +1,3 @@ module Knot - VERSION = "0.1.0" + VERSION = "0.3.2" end