commit 8fcaacd6b9f666dcfb94138e296b2c320fe9799f
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Aug 15 02:34:13 2010 +0200

    [evol] work on cyborg server protocol and API #7 (refs #31)

diff --git a/bin/mapmaker b/bin/mapmaker
index 5dfebd9..3dfa443 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -23,6 +23,7 @@ $: << File.join(File.dirname(__FILE__), "..", "lib")
 
 require 'cyborghood/cyborg'
 require 'cyborghood/cyborg/server'
+require 'cyborghood/services/dns'
 
 
 module CyborgHood
@@ -56,12 +57,38 @@ module CyborgHood
       include CyborgServerInterface
       include CyborgServerDefaultInterface
 
-      class DNS
+      class DNS < Services::DNS
         include CyborgServerInterface
 
-        def zone_exists?(data)
-pp data
-          "coucou"
+        export_parent_methods
+        unexport_method :zones
+
+        def test(data)
+          "coucou: " + data.inspect
+        end
+
+        class Zones
+          include CyborgServerInterface
+
+          def initialize
+            @dns = Services::DNS.new
+          end
+
+          def api_methods
+            @dns.zones
+          end
+
+          def method_missing(method_name, *args)
+            if api_methods.include?(method_name.to_s)
+              Zone.new(method_name.to_s)
+            else
+              super
+            end
+          end
+
+          class Zone < Services::Zone
+            include CyborgServerEmbededInterface
+          end
         end
       end
     end
diff --git a/data/cyborghood/default_config/mapmaker.yaml b/data/cyborghood/default_config/mapmaker.yaml
index 189f152..618e7bc 100644
--- a/data/cyborghood/default_config/mapmaker.yaml
+++ b/data/cyborghood/default_config/mapmaker.yaml
@@ -1,2 +1,3 @@
 ---
-dns: {}
+dns:
+  software: bind
\ No newline at end of file
diff --git a/data/cyborghood/schema/mapmaker.yaml b/data/cyborghood/schema/mapmaker.yaml
index dd3a1bf..e04d2db 100644
--- a/data/cyborghood/schema/mapmaker.yaml
+++ b/data/cyborghood/schema/mapmaker.yaml
@@ -4,4 +4,5 @@ mapping:
   "dns":
     type: map
     mapping:
+      "software": {type: str, enum: [bind]}
       "master_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
diff --git a/lib/cyborghood/base/lang_additions.rb b/lib/cyborghood/base/lang_additions.rb
index 1610d1e..536e827 100644
--- a/lib/cyborghood/base/lang_additions.rb
+++ b/lib/cyborghood/base/lang_additions.rb
@@ -39,6 +39,10 @@ class Object
   def human_name
     self.class.human_name
   end
+
+  def singleton_class
+    class << self; self; end
+  end
 end
 
 class String
@@ -46,4 +50,3 @@ class String
     Float self rescue false
   end
 end
-
diff --git a/lib/cyborghood/cyborg/interface.rb b/lib/cyborghood/cyborg/interface.rb
index e17243e..e8b3354 100644
--- a/lib/cyborghood/cyborg/interface.rb
+++ b/lib/cyborghood/cyborg/interface.rb
@@ -22,7 +22,22 @@ require 'singleton'
 module CyborgHood
   module CyborgServerInterface
     def self.included(base)
+      base.class_eval("include CyborgServerInterfaceBase")
       base.class_eval("include Singleton")
+    end
+  end
+
+  module CyborgServerEmbededInterface
+    def self.included(base)
+      base.class_eval("include CyborgServerInterfaceBase")
+      base.export_parent_methods
+    end
+  end
+
+  module CyborgServerInterfaceBase
+    NODE_PATTERN = "([a-zA-Z0-9._]+(?:\/[a-zA-Z0-9._]+)*[?=]?)"
+
+    def self.included(base)
       base.extend(ClassMethods)
     end
 
@@ -35,9 +50,21 @@ module CyborgHood
         self.exported_methods ||= []
         self.exported_methods += syms.collect{|m| m.to_s }
       end
+
+      def unexport_method(syms)
+        syms = [syms] unless syms.is_a? Array
+        self.exported_methods ||= []
+        self.exported_methods -= syms.collect{|m| m.to_s }
+      end
+
+      def export_parent_methods
+        self.export_method self.superclass.public_instance_methods(false)
+      end
     end
 
-    def initialize
+    def initialize(*args)
+      super(*args)
+
       @config = Config.instance
 
       self.class.exported_methods ||= []
@@ -47,7 +74,7 @@ module CyborgHood
     def api_klasses
       list = self.class.constants.collect do |c|
         cc = self.class.const_get(c)
-        (cc.class == Class and cc.ancestors.include? CyborgServerInterface) ? [c, cc] : nil
+        (cc.class == Class and cc.ancestors.include? CyborgHood::CyborgServerInterfaceBase) ? [c, cc] : nil
       end.compact
       Hash[list]
     end
@@ -63,23 +90,45 @@ module CyborgHood
     end
 
     def find_node_action(node_name)
-      next_node_name, other_nodes_names = node_name.split('.', 2)
+      next_node_name, other_nodes_names = node_name.split('/', 2)
 
       next_node_klass = api_klasses[next_node_name]
+      # inner class or method ?
       if next_node_klass.nil?
+        # method is declared ?
         if api_methods.include? next_node_name
-          next_node_method = self.method(next_node_name)
-          return next_node_method if other_nodes_names.nil?
-          next_node = next_node_method.call
+          # final node ?
+          if other_nodes_names.nil?
+            # cannot use method(), as this method may not exist at all (but still
+            # be usuable through metaprogramming
+            return lambda do |*args|
+              r = self.send(next_node_name, *args)
+              # dynamic tree construction: method may return a node
+              if r.class.ancestors.include? CyborgHood::CyborgServerInterfaceBase
+                r.api_nodes
+              else
+                r
+              end
+            end
+          end
+          next_node = self.send(next_node_name)
         else
+          # unknown method
           return
         end
       else
         next_node = next_node_klass.instance
+        # final node ?
         return next_node.method('api_nodes') if other_nodes_names.nil?
       end
 
-      next_node.find_node_action(other_nodes_names)
+      # search deeper
+      if next_node.class.ancestors.include? CyborgHood::CyborgServerInterfaceBase
+        next_node.find_node_action(other_nodes_names)
+      else
+        # it is not a node, so there are no children
+        return
+      end
     end
 
     def has_node?(cmd)
diff --git a/lib/cyborghood/cyborg/server.rb b/lib/cyborghood/cyborg/server.rb
index 8900e48..08c1beb 100644
--- a/lib/cyborghood/cyborg/server.rb
+++ b/lib/cyborghood/cyborg/server.rb
@@ -81,7 +81,7 @@ module CyborgHood
     MaxLineLength = 16*1024
 
     EOD = "\033[0J"
-    NODE_PATTERN = /^([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*[?=]?)(?: ([+?]+))?$/
+    COMMAND_PATTERN = "^#{CyborgServerInterfaceBase::NODE_PATTERN}(?: ([+?]+))?$"
 
     def initialize(interface)
       @interface = interface
@@ -119,13 +119,16 @@ module CyborgHood
         else
           logger.debug "Received data [#{identifier}]: #{data}"
 
-          unless data =~ NODE_PATTERN
+          unless data =~ Regexp.new(COMMAND_PATTERN)
             logger.error "Error [#{identifier}]: syntax error"
             send_line "552 syntax error in command"
             return
           end
           cmd = $1
           flags = $2 || ""
+pp "GROK"
+pp cmd
+pp flags
 
           if flags.index '?'
             send_line "250+ ok"
diff --git a/lib/cyborghood/services/dns.rb b/lib/cyborghood/services/dns.rb
index a32738a..50620ee 100644
--- a/lib/cyborghood/services/dns.rb
+++ b/lib/cyborghood/services/dns.rb
@@ -18,9 +18,31 @@
 
 require 'fileutils'
 
+# ensure we can find the needed programs (should be handled somewhere else)
+ENV['PATH'] = (ENV['PATH'].split(":") + ["/sbin", "/usr/sbin", "/usr/local/sbin"]).uniq.join(":")
+
 module CyborgHood
   module Services
     class DNS
+      def initialize
+        @config = Config.instance
+
+        @zone_files_pattern = @config.dns.master_zone_pattern.gsub("#ZONE#", "*")
+        @zone_files_regex = Regexp.new("^" + @config.dns.master_zone_pattern.gsub("#ZONE#", "(.*)") + "$")
+      end
+
+      def zones(zone = nil)
+        if zone.nil?
+          Dir.glob(@zone_files_pattern).collect do |file|
+            $1 if file =~ @zone_files_regex
+          end
+        else
+          Zone.new(zone)
+        end
+      end
+    end
+
+    class Zone
       attr_reader :zone
       attr_accessor :content
 
@@ -31,9 +53,6 @@ module CyborgHood
         @content = nil
 
         @filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
-
-        # ensure we can find the needed programs (should be handled somewhere else)
-        ENV['PATH'] = (ENV['PATH'].split(":") + ["/sbin", "/usr/sbin", "/usr/local/sbin"]).uniq.join(":")
       end
 
       def read_zone
@@ -45,16 +64,17 @@ module CyborgHood
       end
 
       def serial
-        case @config.dns.nameserver || :bind
-        when :bind
+        case @config.dns.software
+        when 'bind'
           output = []
           begin
             IO.popen("named-checkzone -i none '#{@zone}' #{@filename}") do |fp|
               output << fp.gets.chomp! until fp.eof?
             end
           rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found")
+            raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found (I/O error)")
           end
+          raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found (#{output.first})") unless $?.success?
 
           if $? == 0
             serial = nil
@@ -73,15 +93,12 @@ module CyborgHood
           else
             nil
           end
-        else
-          # TODO: should be checked at startup time
-          raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
         end
       end
 
       def check_zone_file(filename)
-        case @config.dns.nameserver || :bind
-        when :bind
+        case @config.dns.software
+        when 'bind'
           output = []
           begin
             IO.popen("named-checkzone '#{@zone}' #{filename}") do |fp|
@@ -111,9 +128,6 @@ module CyborgHood
           else
             return {:ok => false, :errors => messages}.to_ostruct
           end
-        else
-          # TODO: should be checked at startup time
-          raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
         end
       end
 
