Project

General

Profile

« Previous | Next » 

Revision 7466fc08

Added by Marc Dequènes over 13 years ago

  • ID 7466fc083799a22deae3d56779823c810028f280

[evol] server API tree reworked using a DSL (reply handling rework is WIP)

View differences:

bin/librarian
include BotNet
def interface
LibrarianInterface.instance
end
end
class LibrarianInterface
include CyborgServerInterface
include CyborgServerRootInterfaceAddon
def setup
super
class Gruik
include CyborgServerInterface
def api_methods
["g1", "g2", "g3"]
define_interface "0.1~" do
node "Gruik" do
node ["g1", "g2", "g3"] do
on_request do |request|
request.reply.results = {
:plouf => ">>> #{node_name} <<<"
}
end
end
end
end
dynamic_interface {|node_name| ">>> #{node_name} <<<" }
end
end
end
bin/mapmaker
class MapMakerValidator < CyborgHoodValidator
def validate_hook_in(value, rule, path, msg_list)
case rule.name
when 'MasterZonePattern'
when 'MasterZonePattern', 'SignedMasterZonePattern', 'SlaveZonePattern'
if value.gsub('#ZONE#', 'test') == value
msg_list << "pattern is constant"
end
......
include BotNet
def interface
MapMakerInterface.instance
end
end
class MapMakerInterface
include CyborgServerInterface
include CyborgServerRootInterfaceAddon
class Taiste
include CyborgServerInterface
def api_methods
["coucou", "toto", "plop"]
end
dynamic_interface {|node_name| ">>> #{node_name} <<<" }
end
class DNS < Services::DNS::System
include CyborgServerInterface
export_parent_methods
unexport_method :zones, :'[]'
def test(data)
"coucou: " + data.inspect
end
class Zones
include CyborgServerStatefulInterface
def initialize
@dns = Services::DNS::System.new
end
def api_methods
@dns.zones
end
def api_container_methods
api_methods
def setup
super
define_interface "0.1~" do
node "DNS" do
dns = Services::DNS::System.new
on_request do |request|
request.reply.results = {
:type => dns.type
}
end
node "check_config" do
on_request do |request|
request.reply.results = dns.check_config
end
end
node "Zones" do
zone_list = Proc.new{dns.zones}
node zone_list do
zone = Services::DNS::Zone.new(node_name)
on_request do |request|
request.reply.results = {
:master => zone.master?,
:signed => zone.signed?,
:serial => zone.serial_in_dns
}
if zone.master?
reply.results.merge!({
:serial_in_zone_file => zone.serial_in_zone_file,
:serial_in_signed_zone_file => zone.serial_in_signed_zone_file
})
end
end
node "content" do
on_request do |request|
request.reply.results = {:content => zone.content}
end
end
node "content=" do
on_request do |request|
content = request.args.shift
if content.nil?
request.errors << "Zone content missing"
end
zone.content = content
if zone.changed?
check_result = zone.check
if check_result[:ok]
reply.warnings = check_result[:warnings]
# zone signer automatically handles serial bump
if check_result[:serial] > zone.serial or zone.signed?
zone.save
zone.activate
else
reply.errors << _("Zone serial is not superior to current serial.")
end
else
reply.errors = check_result[:errors]
zone.cancel_changes
end
else
reply.warnings << _("Zone is unmodified (same content)")
zone.cancel_changes
end
end
end
end
end
end
stateful_dynamic_interface("DnsZone/#NODE#") {|node_name| DnsZone.new(node_name) }
end
end # interface
end
end
class DnsZone < Services::DNS::Zone
include CyborgServerEmbeddedInterface
end
end # class MapMaker
end
end
bin/test_client
toto = true
task "compare stuff" do
ask "MapMaker", :ver1, "/api_version"
ask "Librarian", :ver2, "/api_version"
ask "MapMaker", :info1, "/_cyborg_"
ask "Librarian", :info2, "/_cyborg_"
ask "MapMaker", :zones, "/DNS/Zones"
on_error do
puts "PLOUF"
......
puts "OK"
pp errors
pp results
puts "Tadam: " + (results[:ver1] == results[:ver2] ? "same" : "different")
puts "Tadam: " + (results[:info1][:api_version] == results[:info2][:api_version] ? "same" : "different")
meet "waiter", :zzz
STDOUT.flush
on_success do
puts "OK compare stuff"
end
end
on_error do
stop_bot :at_once
end
end
task "waiter" do
meet "compare stuff", :zzz
on_success do
puts "OK waiter"
#stop_bot :at_once
stop_bot :when_finished
end
end
# conversation_with "MapMaker" do
# on_error do
# puts "Halalalala !"
# end
# thread "super taiste" do
# call :ver, "/api_version"
# call :zones, "/DNS/Zones" if toto
# on_error do
# puts "Sniff !"
# pp reply
# end
# on_success do
# puts "Yahou !"
# pp reply
# call :gogogo, "/DNS"
# on_error do
# pp "Plouf"
# pp reply
# end
# on_success do
# pp "Hop!"
# pp reply
# send_notification 'meetpoint', { :topic => "MYNOTIF", :msg => "plop" }
# end
# end
# end
# #stop_bot :at_once
# stop_bot :when_finished
# end
# conversation_with "Librarian" do
# thread "taistouille" do
# call :sdf, "/Gruik"
# on_success do
# puts "Librarian GoGoGo!"
# pp reply
# wait_notification 'meetpoint', { :topic => "MYNOTIF" }
# on_success do
# puts "NOTIF!"
# pp reply
# end
# end
# end
# end
end
end
end
data/cyborghood/schema/mapmaker.yaml
mapping:
"software": {type: str, enum: [bind]}
"master_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
"signed_master_zone_pattern": {type: str, name: MasterZonePattern}
"slave_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
"update_zone_script": {type: str}
lib/cyborghood/cyborg/botnet.rb
require 'set'
module CyborgHood
# default interface if not overridden
# a mere "BotClient" would then always have a default basic interface
class EmptyInterface
include CyborgServerInterface
include CyborgServerRootInterfaceAddon
end
module BotNet
attr_reader :interface
......
@comm_list = {}
@comm_list_attempt = {}
self.interface.bot = self
# default empty interface
define_interface("0") {}
end
def define_interface(version, &block)
@interface = DSL::ServerApiNode.new(self, &block)
@interface.add_behavior do
node "_cyborg_" do
on_request do |request|
request.reply.results = {
:name => bot.name,
:product_name => PRODUCT,
:product_version => VERSION,
:api_version => version
}
end
end
end
end
def contact_peer(peer, block)
......
super
end
def interface
EmptyInterface.instance
end
protected
def process_system_notification(msg)
lib/cyborghood/cyborg/botnet/interface.rb
#++
require 'singleton'
require 'ostruct'
module CyborgHood
# the base mixin (not intended to be used directly, but...)
module CyborgServerInterfaceBase
NODE_PATTERN = "((?:\/|(?:\/[a-zA-Z0-9._]+)+[?=]?))"
module DSL
class ServerApiNode < BaseDSL
attr_reader :bot
attr_accessor :bot
# needed for testing node existence
reveal :nil?
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
attr_accessor :exported_methods
attr_accessor :auto_export_public_instance_methods
def initialize(bot, parent_node = nil, &block)
@bot = bot
@parent_node = parent_node
# don't call super because we need defered loading
@blocks = [block]
def export_method(*syms)
syms = [syms] unless syms.is_a? Array
self.exported_methods ||= []
self.exported_methods += syms.collect{|m| m.to_s }
cleanup
end
def unexport_method(*syms)
syms = [syms] unless syms.is_a? Array
self.exported_methods ||= []
self.exported_methods -= syms.collect{|m| m.to_s }
def add_behavior(&block)
@blocks << block
end
def export_parent_methods
self.export_method *self.superclass.public_instance_methods(false)
# string, array (useful for aliases), or regex
# TODO: name validation
def node(match, &block)
child_node = self.class.new(@bot, self, &block)
if match.is_a? Array
match.each{|n| @nodes[n] = child_node}
else
@nodes[match] = child_node
end
end
def is_node?(node)
(node =~ Regexp.new(NODE_PATTERN)) ? true : false
def on_request(&cb)
@request_cb = cb
end
end
def initialize(*args)
super(*args)
@config = Config.instance
self.class.exported_methods ||= []
self.class.auto_export_public_instance_methods = true
end
# convenience method
def is_node?(node)
self.class.is_node?(node)
end
def _is_node?(session, node_path)
node = __send__(:find_node, session, node_path)
not node.nil?
end
def api_klasses
list = self.class.constants.collect do |c|
cc = self.class.const_get(c)
(cc.class == Class and cc.ancestors.include? CyborgHood::CyborgServerInterfaceBase) ? [c, cc] : nil
end.compact
Hash[list]
end
def _call(session, node_path, args = nil)
args ||= []
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless args.is_a? Array
def api_methods
methods = []
methods += self.class.public_instance_methods(false) if self.class.auto_export_public_instance_methods
methods -= ["initialize", "__destroy", "method_missing"]
methods &= self.methods
methods += self.class.exported_methods
end
node = find_node(session, node_path)
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if node.nil?
def api_container_methods
[]
end
logger.debug "[Server API] Node '#{node_path}' found"
r = node.__send__(:request, session, args)
logger.debug "[Server API] reply for node '#{node_path}': " + r.inspect
r
end
def api_containers
(api_klasses.keys + api_container_methods).sort
end
protected
def api_leafs
(api_methods - api_container_methods).sort
end
def root?
@parent_node.nil?
end
def api_nodes
(api_klasses.keys + api_methods).sort
end
def load(node_element = '')
cleanup
@node_name = node_element
@blocks.each do |bl|
instance_eval &bl
end
end
def find_node_action(session, node_name)
node_name.gsub!(/^\//, "")
next_node_name, other_nodes_names = node_name.split('/', 2)
def find_node(session, node_path)
# it is a string argument when interface root is called, but a list of node elemnts later
if root?
logger.debug "[Server API] Looking for node '#{node_path}'"
node_path = node_path.split("/")
# remove empty string before first "/"
node_path.shift
# initial load
load
end
next_node_klass = next_node_name.nil? ? self.class : api_klasses[next_node_name]
# inner class or method ?
if next_node_klass.nil?
# method is declared ?
if api_methods.include? next_node_name
# final node ?
if other_nodes_names.blank?
# cannot use method(), as this method may not exist at all (but still
# be usuable through metaprogramming
return lambda do |*args|
r = child_node(next_node_name, session, *args)
# dynamic tree construction: method may return a node
if r.is_a? CyborgHood::CyborgServerInterfaceBase
r.api_nodes
else
r
end
end
end
# not a container, leaving
return unless self.api_container_methods.include? next_node_name
next_node = child_node(next_node_name, session)
node_element = node_path.shift
logger.debug "[Server API] Looking for node element '#{node_element}'"
if node_element.nil?
return self
else
# unknown method
return
next_node = find_child_node(node_element)
if next_node
next_node.__send__(:load, node_element)
return next_node.__send__(:find_node, session, node_path)
else
return
end
end
else
next_node = next_node_klass.instance
# final node ?
return next_node.method('api_nodes') if other_nodes_names.blank?
end
# search deeper
if next_node.is_a? CyborgHood::CyborgServerInterfaceBase
next_node.find_node_action(session, other_nodes_names)
else
# it is not a node, so there are no children
return
end
end
def child_node(next_node_name, session, *args)
args.unshift session if self.is_a? CyborgHood::CyborgServerStatefulInterface
self.send(next_node_name, *args)
end
def has_node?(cmd)
not find_node_action(nil, cmd).nil?
end
# preliminary incoming message handling
def call(session, cmd, data = nil)
action = find_node_action(session, cmd)
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if action.nil?
data ||= []
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless data.is_a? Array
def find_child_node(child_node)
return @nodes[child_node] if @nodes.has_key? child_node
@nodes.each_pair do |match, node|
found = if match.is_a? String
child_node == match
elsif match.is_a? Regexp
child_node =~ Regexp.new(match)
elsif match.is_a? Proc
match.call.include? child_node
end
return node if found
end
begin
action.call(*data)
rescue
logger.debug "node action error message: " + $!
logger.debug "node action error backtrace: " + $!.backtrace.join("\n")
raise CyberError.new(:unrecoverable, 'api/cyborghood', "method call failed: " + $!)
nil
end
end
end
# structural mixins
module CyborgServerInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.class_eval("include Singleton")
base.extend(ClassMethods)
end
class Request
attr_reader :session, :args, :reply
module ClassMethods
def dynamic_interface(&resource_generator)
class_eval do
class_inheritable_reader :resource_generator
def initialize(session, args)
@session = session
@args = args
def method_missing(method_name, *args)
node_name = method_name.to_s
if api_methods.include?(node_name)
self.resource_generator.call(node_name)
else
super
end
end
@reply = {
:results => {},
:infos => [],
:warnings => [],
:errors => []
}.to_ostruct
end
write_inheritable_attribute(:resource_generator, resource_generator)
end
end
end
module CyborgServerEmbeddedInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.export_parent_methods
end
end
module CyborgServerStatefulInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.class_eval("include Singleton")
base.extend(ClassMethods)
end
module ClassMethods
def stateful_dynamic_interface(resource_key_pattern, &resource_generator)
class_eval do
class_inheritable_reader :resource_key_pattern, :resource_generator
def method_missing(method_name, *args)
session = args.shift
node_name = method_name.to_s
if api_methods.include?(node_name)
resource_key = self.resource_key_pattern.gsub("#NODE#", node_name)
session.store.get(resource_key) { self.resource_generator.call(node_name) }
else
super
end
def request(session, args)
if @request_cb
request = Request.new(session, args)
begin
@request_cb.call(request)
# TODO: full reply needed
request.reply.results
rescue
logger.debug "node request error message: " + $!
logger.debug "node request error backtrace: " + $!.backtrace.join("\n")
raise CyberError.new(:unrecoverable, 'api/cyborghood', "method call failed: " + $!)
end
else
@nodes.keys.collect do |match|
if match.is_a? String
match
elsif match.is_a? Regexp
'/' + match.to_s + '/'
elsif match.is_a? Proc
match.call
end
end.compact.flatten
end
write_inheritable_attribute(:resource_key_pattern, resource_key_pattern)
write_inheritable_attribute(:resource_generator, resource_generator)
end
end
end
# additional mixin
module CyborgServerRootInterfaceAddon
API_VERSION = "0.1~"
def self.included(base)
list = self.public_instance_methods(false)
base.class_eval do
export_method *list
def cleanup
@nodes = {}
@request_cb = nil
end
end
def product_name
PRODUCT
end
def product_version
VERSION
end
def api_version
API_VERSION
end
def bot_name
@bot.name
end
end
end
lib/cyborghood/cyborg/botnet/protocol.rb
if message.action_parameters.nil?
return send_error_action(message, "missing parameters")
end
unless @conversation.bot.interface.is_node? message.action_parameters[:node]
unless @conversation.bot.interface._is_node?(message.conv_thread.session, message.action_parameters[:node])
return send_error_action(message, "bad node")
end
send_reply_ack(message)
......
:reply_message => message
}
begin
result[:action_result] = @conversation.bot.interface.call(message.conv_thread.session,
result[:action_result] = @conversation.bot.interface._call(message.conv_thread.session,
message.action_parameters[:node],
message.action_parameters[:parameters])
rescue CyberError => e
......
if message.action_parameters.nil?
return send_error_action(message, "missing parameters")
end
unless @conversation.bot.interface.is_node? message.action_parameters[:node]
unless @conversation.bot.interface._is_node?(message.conv_thread.session, message.action_parameters[:node])
return send_error_action(message, "bad node")
end
send_reply_ack(message)
lib/cyborghood/cyborg/dsl.rb
module DSL
class BaseDSL < ActiveSupport::BasicObject
def initialize(&block)
self.instance_eval(&block)
_load_block(&block)
_start_dsl
end
reveal :class
reveal :logger
protected
def _load_block(&block)
self.instance_eval(&block)
end
def _start_dsl
end
end
class Task < BaseDSL
lib/cyborghood/services/dns.rb
end
end
def zones
master_zones + slave_zones
end
def type
@config.dns.software
end
......
end
class Zone
def initialize(zone)
@zone = zone
def initialize(name)
@name = name
@config = Config.instance
@resolver = Dnsruby::Resolver.new
......
raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
end
end
system "sudo #{script} '#{@zone}' >/dev/null"
system "sudo #{script} '#{@name}' >/dev/null"
raise CyberError.new(:unrecoverable, "services/dns", "zone activation failed") unless $?.success?
end
......
return unless @temp_file.nil?
begin
@temp_file = Tempfile.new(@zone)
@temp_file = Tempfile.new(@name)
@temp_file.write(@content)
@temp_file.close
rescue
......
@content = File.read(filename)
update_hash if filename == @filename
rescue
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' cannot be read from '#{filename}' (I/O error, nonexistent or lack of permission)")
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be read from '#{filename}' (I/O error, nonexistent or lack of permission)")
end
end
......
fp.print @content
end
rescue
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' cannot be written to '#{filename}' (I/O error or lack of permission)")
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be written to '#{filename}' (I/O error or lack of permission)")
end
end
......
when 'bind'
output = []
begin
IO.popen("named-checkzone -i #{check_type} '#{@zone}' #{filename}") do |fp|
IO.popen("named-checkzone -i #{check_type} '#{@name}' #{filename}") do |fp|
output << fp.gets.chomp! until fp.eof?
end
rescue
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' could not be checked (I/O error)")
raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' could not be checked (I/O error)")
end
serial = nil

Also available in: Unified diff