commit 90197e7bd70d31f0ef7cd6dde8f2e95b6dc08ab5
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Tue Mar 1 03:15:55 2011 +0100

    [evol] MapMaker API and internals rework §1, going forward to Dependency Injection using Needle

diff --git a/bin/mapmaker b/bin/mapmaker
index 4c5c5e6..06b7c3f 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -21,8 +21,8 @@
 # to allow in-place run for test
 $: << File.join(File.dirname(__FILE__), "..", "lib")
 
+require 'needle'
 require 'cyborghood/cyborg'
-require 'cyborghood-mapmaker/dns'
 
 
 module CyborgHood
@@ -40,7 +40,7 @@ module CyborgHood
     end
   end
 
-  module MapMakerHome
+  module MapMakerLand
     include I18nTranslation
     bindtextdomain("cyborghood_mapmaker", {:path => Config::L10N_DIR, :charset => "UTF-8"})
 
@@ -59,7 +59,15 @@ module CyborgHood
   end
 end
 
-bot = CyborgHood::MapMakerHome::MapMaker.new
+reg = Needle::Registry.new
+reg.define do |b|
+  b.require 'cyborghood', CyborgHood
+  b.require 'cyborghood-mapmaker/land', CyborgHood::MapMakerLand
+
+  b.bot { CyborgHood::MapMakerLand::MapMaker.new(b.mapmaker_land) }
+end
+
+bot = reg.bot
 
 trap('INT') do
   bot.ask_to_stop
diff --git a/bin/test_client b/bin/test_client
index 749c4c9..9a468b8 100755
--- a/bin/test_client
+++ b/bin/test_client
@@ -41,8 +41,9 @@ module CyborgHood
           ask "Librarian", :info2, "/_cyborg_"
           ask "MapMaker", :zones, "/Zones"
           #ask "MapMaker", :wanted_failure, "/prout"
-          ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
           ask "MapMaker", :dns, "/Services/DNS"
+          ask "MapMaker", :dnssec, "/Services/DNSSEC"
+          ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
           ask "MapMaker", :dns_check, "/Services/DNS/check_config"
           #ask "MapMaker", :search, "/Zones/?"
           #ask "MapMaker", :search_master, "/Zones/?", {:master => true}
diff --git a/data/cyborghood/default_config/mapmaker.yaml b/data/cyborghood/default_config/mapmaker.yaml
index 618e7bc..d1a3dcd 100644
--- a/data/cyborghood/default_config/mapmaker.yaml
+++ b/data/cyborghood/default_config/mapmaker.yaml
@@ -1,3 +1,5 @@
 ---
 dns:
-  software: bind
\ No newline at end of file
+  software: bind
+dnssec:
+  software: opendnssec
diff --git a/data/cyborghood/schema/mapmaker.yaml b/data/cyborghood/schema/mapmaker.yaml
index 61f7b49..c7cc13c 100644
--- a/data/cyborghood/schema/mapmaker.yaml
+++ b/data/cyborghood/schema/mapmaker.yaml
@@ -9,3 +9,7 @@ mapping:
       "signed_master_zone_pattern": {type: str, name: MasterZonePattern}
       "slave_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
       "update_zone_script": {type: str}
+  "dnssec":
+    type: map
+    mapping:
+      "software": {type: str, enum: [opendnssec]}
diff --git a/lib/cyborghood-mapmaker/dns.rb b/lib/cyborghood-mapmaker/dns.rb
deleted file mode 100644
index c4b55e7..0000000
--- a/lib/cyborghood-mapmaker/dns.rb
+++ /dev/null
@@ -1,353 +0,0 @@
-#--
-# CyborgHood, a distributed system management software.
-# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#++
-
-require 'digest/md5'
-require 'tempfile'
-require 'fileutils'
-require 'dnsruby'
-
-# 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
-    module DNS
-      class System
-        def initialize
-          @config = Config.instance
-
-          @master_zone_files_pattern = @config.dns.master_zone_pattern.gsub("#ZONE#", "*")
-          @master_zone_files_regex = Regexp.new("^" + @config.dns.master_zone_pattern.gsub("#ZONE#", "(.*)") + "$")
-          @slave_zone_files_pattern = @config.dns.slave_zone_pattern.gsub("#ZONE#", "*")
-          @slave_zone_files_regex = Regexp.new("^" + @config.dns.slave_zone_pattern.gsub("#ZONE#", "(.*)") + "$")
-        end
-
-        def master_zones
-          Dir.glob(@master_zone_files_pattern).collect do |file|
-            $1 if file =~ @master_zone_files_regex
-          end
-        end
-
-        def slave_zones
-          Dir.glob(@slave_zone_files_pattern).collect do |file|
-            $1 if file =~ @slave_zone_files_regex
-          end
-        end
-
-        def zones
-          master_zones + slave_zones
-        end
-
-        def type
-          @config.dns.software
-        end
-
-        def check_config
-          case @config.dns.software
-          when 'bind'
-            output = []
-            begin
-              IO.popen("sudo named-checkconf") do |fp|
-                output << fp.gets.chomp! until fp.eof?
-              end
-            rescue
-              raise CyberError.new(:unrecoverable, "services/dns", "global configuration could not be checked (I/O error)")
-            end
-
-            if $?.success?
-              return {:ok => true, :warnings => output}
-            else
-              return {:ok => false, :errors => output}
-            end
-          end
-        end
-
-        def status
-          # TODO: use a factory
-          case @config.dns.software || 'bind'
-          when 'bind'
-            script = "rndc status"
-          else
-            # TODO: should be checked at startup time
-            raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
-          end
-          output = []
-          begin
-            IO.popen("sudo #{script}") do |fp|
-              output << fp.gets.chomp! until fp.eof?
-            end
-          rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "could not get DNS server status")
-          end
-
-          status = {}
-          output.each do |line|
-            line.sub!(" is ", ": ")
-            key, value = line.split(": ")
-            next if key.nil? or value.nil?
-
-            key.gsub!(" ", "_")
-            status[key.to_sym] = value.strip
-          end
-
-          status
-        end
-      end
-
-      class Zone
-        def initialize(name)
-          @name = name
-
-          @config = Config.instance
-          @resolver = Dnsruby::Resolver.new
-          @content = nil
-          @temp_file = nil
-
-          master_filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @name)
-          slave_filename = @config.dns.slave_zone_pattern.gsub("#ZONE#", @name)
-          if File.exists? master_filename
-            @master = true
-            @filename = master_filename
-            @filename_signed = @config.dns.signed_master_zone_pattern.gsub("#ZONE#", @name) if @config.dns.signed_master_zone_pattern
-          elsif File.exists? slave_filename
-            @master = false
-            @filename = slave_filename
-          else
-            raise CyberError.new(:unrecoverable, "services/dns", "nonexistent zone '#{@name}'")
-          end
-        end
-
-        def master?
-          @master
-        end
-
-        def has_signed_zone_file?
-          master? and File.exists?(@filename_signed)
-        end
-
-        def signed?
-          # check if really signed in DNS
-          logger.debug "Querying DNSKEY for domain '#{@name}'"
-          not @resolver.query(@name, 'DNSKEY').answer.empty?
-        end
-
-        def serial_in_dns
-          soa = @resolver.query(@name, 'SOA').answer{|rr| rr.type == 'SOA'}.first
-          soa ? soa.serial : nil
-        end
-        alias_method :serial, :serial_in_dns
-
-        def serial_in_zone_file
-          reader = Dnsruby::ZoneReader.new(@name)
-          begin
-            zone = reader.process_file(@filename)
-            soa = zone.select{|rr| rr.name.to_s == @name and rr.type == 'SOA' }.first
-            soa ? soa.serial : nil
-          rescue Dnsruby::ZoneReader::ParseException
-            logger.warn "Problem parsing DNS zone '#{@name}'"
-          end
-        end
-
-        def serial_in_signed_zone_file
-          return unless has_signed_zone_file?
-
-          reader = Dnsruby::ZoneReader.new(@name)
-          begin
-            zone = reader.process_file(@filename_signed)
-            soa = zone.select{|rr| rr.name.to_s == @name and rr.type == 'SOA' }.first
-            soa ? soa.serial : nil
-          rescue Dnsruby::ZoneReader::ParseException
-            logger.warn "Problem parsing DNS zone '#{@name}'"
-          end
-        end
-
-        def content
-          read_zone(@filename) if @content.nil?
-          @content
-        end
-
-        def content=(txt)
-          @content = txt
-        end
-
-        def changed?
-          return false if @content.nil?
-
-          # if original hash is missing, save zone content, reload
-          # original file, compute hash, and restore previous content
-          if @content_hash.nil?
-            content_backup = @content
-            @content = nil
-            content
-            @content = content_backup
-          end
-
-          Digest::MD5.hexdigest(@content) != @content_hash
-        end
-
-        def cancel_changes
-          @content = nil
-          cleanup_temp
-        end
-
-        def check(force_real = false)
-          check_zone_file('full', force_real = false)
-        end
-
-        def import_from_file(new_zone_filename)
-          read_zone(new_zone_filename)
-        end
-
-        def import_from_backup
-          read_zone(self.backup_filename)
-        end
-
-        def create_backup
-          FileUtils.cp(@filename, backup_filename())
-        end
-
-        def save
-          raise CyberError.new(:unrecoverable, "services/dns", "won't save an empty zone file") if @content.nil?
-          write_zone(@filename)
-          update_hash
-          cleanup_temp
-        end
-
-        def activate
-          script = @config.dns.update_zone_script
-          if script.nil?
-            # TODO: use a factory
-            case @config.dns.software || 'bind'
-            when 'bind'
-              script = "rndc reload"
-            else
-              # TODO: should be checked at startup time
-              raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
-            end
-          end
-          system "sudo #{script} '#{@name}' >/dev/null"
-          raise CyberError.new(:unrecoverable, "services/dns", "zone activation failed") unless $?.success?
-        end
-
-        def __destroy
-          cleanup_temp
-        end
-
-        protected
-
-        def backup_filename
-          @filename + ".ch-backup"
-        end
-
-        def save_to_temp
-          return unless @temp_file.nil?
-
-          begin
-            @temp_file = Tempfile.new(@name)
-            @temp_file.write(@content)
-            @temp_file.close
-          rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "could not save temporary zone")
-          end
-        end
-
-        def cleanup_temp
-          return if @temp_file.nil?
-
-          @temp_file.close!
-          @temp_file = nil
-        end
-
-        def temp_filename
-          @temp_file.path
-        end
-
-        def current_filename
-          if changed?
-            save_to_temp
-            return temp_filename
-          end
-
-          @filename
-        end
-
-        def update_hash
-          @content_hash = Digest::MD5.hexdigest(@content)
-        end
-
-        def read_zone(filename)
-          begin
-            @content = File.read(filename)
-            update_hash if filename == @filename
-          rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be read from '#{filename}' (I/O error, nonexistent or lack of permission)")
-          end
-        end
-
-        def write_zone(filename)
-          begin
-            File.open(filename, "w") do |fp|
-              fp.print @content
-            end
-          rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be written to '#{filename}' (I/O error or lack of permission)")
-          end
-        end
-
-        def check_zone_file(check_type, force_real = false)
-          filename = force_real ? @filename : current_filename
-
-          # TODO: use a factory
-          case @config.dns.software
-          when 'bind'
-            output = []
-            begin
-              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 '#{@name}' could not be checked (I/O error)")
-            end
-
-            serial = nil
-            messages = []
-            output.each do |l|
-              next if l == "OK"
-              if l =~ /: loaded serial (\d+)$/
-                serial = $1
-                next
-              end
-              messages << l
-            end
-
-            if $?.success?
-              if serial
-                return {:ok => true, :serial => serial, :warnings => messages}.to_ostruct
-              else
-                raise CyberError.new(:unrecoverable, "services/dns", "zone validated but no serial returned")
-              end
-            else
-              return {:ok => false, :errors => messages}.to_ostruct
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/cyborghood-mapmaker/dns/base.rb b/lib/cyborghood-mapmaker/dns/base.rb
new file mode 100644
index 0000000..4335e5b
--- /dev/null
+++ b/lib/cyborghood-mapmaker/dns/base.rb
@@ -0,0 +1,286 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'digest/md5'
+require 'tempfile'
+require 'fileutils'
+require 'cyborghood-mapmaker/zone_content'
+
+
+module CyborgHood
+  module MapMakerLand
+    class DNSBase
+      attr_reader :master_zones, :slave_zones
+
+      def initialize(config)
+        @config = config
+
+        @master_zone_files_pattern = @config.dns.master_zone_pattern.gsub("#ZONE#", "*")
+        @master_zone_files_regex = Regexp.new("^" + @config.dns.master_zone_pattern.gsub("#ZONE#", "(.*)") + "$")
+        @slave_zone_files_pattern = @config.dns.slave_zone_pattern.gsub("#ZONE#", "*")
+        @slave_zone_files_regex = Regexp.new("^" + @config.dns.slave_zone_pattern.gsub("#ZONE#", "(.*)") + "$")
+
+        flush_cache
+      end
+
+      def zones
+        self.master_zones + self.slave_zones
+      end
+
+      def get_zone(zone_name)
+        self.zones.include?(zone_name) ? DNSZone.new(zone_name) : nil
+      end
+
+      def get_zone_file(zone_name)
+        self.zones.include?(zone_name) ? DNSZoneFile.new(@config, zone_name) : nil
+      end
+
+      def software
+        @config.dns.software
+      end
+
+      def info
+        {
+          :software  => self.software
+        }
+      end
+
+      def flush_cache
+        # TODO: fetch more zone info in bind config
+
+        # master zones
+        @master_zones = Dir.glob(@master_zone_files_pattern).collect do |file|
+          $1 if file =~ @master_zone_files_regex
+        end
+
+        # slave zones
+        @slave_zones = Dir.glob(@slave_zone_files_pattern).collect do |file|
+          $1 if file =~ @slave_zone_files_regex
+        end
+      end
+
+      # methods a backend MUST implement
+      #   - check_config
+      #   - status
+    end
+
+    class DNSZone < ZoneContentBase
+      def initialize(name)
+        super
+
+        @resolver = Dnsruby::Resolver.new
+      end
+
+      def find_rr(rr_type)
+        logger.debug "Querying '#{rr_type}' for domain '#{@name}'"
+        soa = @resolver.query(@name, rr_type).answer{|rr| rr.type == rr_type}
+      end
+
+      def info
+        {
+          :serial     => self.serial,
+          :is_signed  => self.signed?
+        }
+      end
+    end
+
+    class DNSZoneFileBase
+      attr_reader :filename, :filename_signed
+
+      def initialize(config, name)
+        @config = config
+        @name = name
+
+        @content = nil
+        @temp_file = nil
+
+        master_filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @name)
+        slave_filename = @config.dns.slave_zone_pattern.gsub("#ZONE#", @name)
+        if File.exists? master_filename
+          @master = true
+          @filename = master_filename
+          @filename_signed = @config.dns.signed_master_zone_pattern.gsub("#ZONE#", @name) if @config.dns.signed_master_zone_pattern
+        elsif File.exists? slave_filename
+          @master = false
+          @filename = slave_filename
+        else
+          raise CyberError.new(:unrecoverable, "services/dns", "nonexistent zone '#{@name}'")
+        end
+      end
+
+      def master?
+        @master
+      end
+
+      def has_signed_zone_file?
+        master? and File.exists?(@filename_signed)
+      end
+
+      def content
+        read_zone(@filename) if @content.nil?
+        @content
+      end
+
+      def signed_content
+        read_zone(@filename_signed) unless @filename_signed.nil?
+      end
+
+      def content=(c)
+        if not master?
+          raise CyberError.new(:unrecoverable, "services/dns", "cannot change content of non-master zone '#{@name}'")
+        end
+        @content = c.to_s
+      end
+
+      def parsed_content(on_disk = false)
+        parsed_content = ZoneContent.new(@name)
+        if on_disk or not changed?
+          parsed_content.import_from_file(@filename)
+        else
+          parsed_content.content = @content
+        end
+        parsed_content
+      end
+
+      def parsed_signed_content
+        return if @filename_signed.nil?
+
+        parsed_content = ZoneContent.new(@name)
+        parsed_content.import_from_file(@filename_signed)
+        parsed_content
+      end
+
+      def changed?
+        return false if @content.nil?
+
+        # if original hash is missing, save zone content, reload
+        # original file, compute hash, and restore previous content
+        if @content_hash.nil?
+          content_backup = @content
+          @content = nil
+          content
+          @content = content_backup
+        end
+
+        Digest::MD5.hexdigest(@content) != @content_hash
+      end
+
+      def cancel_changes
+        @content = nil
+        cleanup_temp
+      end
+
+      def import_from_file(new_zone_filename)
+        read_zone(new_zone_filename)
+      end
+
+      def import_from_backup
+        read_zone(self.backup_filename)
+      end
+
+      def create_backup
+        FileUtils.cp(@filename, backup_filename())
+      end
+
+      def save
+        raise CyberError.new(:unrecoverable, "services/dns", "won't save an empty zone file") if @content.nil?
+        write_zone(@filename)
+        update_hash
+        cleanup_temp
+      end
+
+      def info
+        {
+          :is_master                   => self.master?,
+          :has_signed_zone_file        => self.has_signed_zone_file?,
+          :serial_in_zone_file         => self.parsed_content.serial,
+          :serial_in_signed_zone_file  => self.parsed_signed_content.serial
+        }
+      end
+
+      def __destroy
+        cleanup_temp
+      end
+
+      # methods a backend MUST implement
+      # - activate
+      # - check
+
+      protected
+
+      def backup_filename
+        @filename + ".ch-backup"
+      end
+
+      def save_to_temp
+        return unless @temp_file.nil?
+
+        begin
+          @temp_file = Tempfile.new(@name)
+          @temp_file.write(@content)
+          @temp_file.close
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "could not save temporary zone")
+        end
+      end
+
+      def cleanup_temp
+        return if @temp_file.nil?
+
+        @temp_file.close!
+        @temp_file = nil
+      end
+
+      def temp_filename
+        @temp_file.path
+      end
+
+      def current_filename
+        if changed?
+          save_to_temp
+          return temp_filename
+        end
+
+        @filename
+      end
+
+      def update_hash
+        @content_hash = Digest::MD5.hexdigest(@content)
+      end
+
+      def read_zone(filename)
+        begin
+          @content = File.read(filename)
+          update_hash if filename == @filename
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be read from '#{filename}' (I/O error, nonexistent or lack of permission)")
+        end
+      end
+
+      def write_zone(filename)
+        begin
+          File.open(filename, "w") do |fp|
+            fp.print @content
+          end
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@name}' cannot be written to '#{filename}' (I/O error or lack of permission)")
+        end
+      end
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood-mapmaker/dns/bind.rb b/lib/cyborghood-mapmaker/dns/bind.rb
new file mode 100644
index 0000000..e6f2e2a
--- /dev/null
+++ b/lib/cyborghood-mapmaker/dns/bind.rb
@@ -0,0 +1,117 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'cyborghood-mapmaker/dns/base'
+
+
+module CyborgHood
+  module MapMakerLand
+    class DNS < DNSBase
+      def check_config
+        output = []
+        begin
+          IO.popen("sudo named-checkconf") do |fp|
+            output << fp.gets.chomp! until fp.eof?
+          end
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "global configuration could not be checked (I/O error)")
+        end
+
+        if $?.success?
+          return {:ok => true, :warnings => output}
+        else
+          return {:ok => false, :errors => output}
+        end
+      end
+
+      def info
+        super.merge(self.status)
+      end
+
+      def status
+        output = []
+        begin
+          IO.popen("sudo rndc status") do |fp|
+            output << fp.gets.chomp! until fp.eof?
+          end
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "could not get DNS server status")
+        end
+
+        status = {}
+        output.each do |line|
+          line.sub!(" is ", ": ")
+          key, value = line.split(": ")
+          next if key.nil? or value.nil?
+
+          key.gsub!(" ", "_")
+          status[key.to_sym] = value.strip
+        end
+
+        status
+      end
+    end
+
+    class DNSZoneFile < DNSZoneFileBase
+      def activate
+        system "sudo rndc reload '#{@name}' >/dev/null"
+        raise CyberError.new(:unrecoverable, "services/dns", "zone activation failed") unless $?.success?
+      end
+
+      def check(on_disk = false)
+        check_zone_file('full', on_disk = false)
+      end
+
+      protected
+
+      def check_zone_file(check_type, on_disk = false)
+        filename = on_disk ? @filename : current_filename
+
+        output = []
+        begin
+          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 '#{@name}' could not be checked (I/O error)")
+        end
+
+        serial = nil
+        messages = []
+        output.each do |l|
+          next if l == "OK"
+          if l =~ /: loaded serial (\d+)$/
+            serial = $1
+            next
+          end
+          messages << l
+        end
+
+        if $?.success?
+          if serial
+            return {:ok => true, :serial => serial, :warnings => messages}.to_ostruct
+          else
+            raise CyberError.new(:unrecoverable, "services/dns", "zone validated but no serial returned")
+          end
+        else
+          return {:ok => false, :errors => messages}.to_ostruct
+        end
+      end
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood-mapmaker/dnssec/base.rb b/lib/cyborghood-mapmaker/dnssec/base.rb
new file mode 100644
index 0000000..3a646d0
--- /dev/null
+++ b/lib/cyborghood-mapmaker/dnssec/base.rb
@@ -0,0 +1,75 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+module CyborgHood
+  module MapMakerLand
+    class DNSSECBase
+      def initialize(config)
+        @config = config
+
+        flush_cache
+      end
+
+      def zones
+        @zone_list.keys
+      end
+
+      def get_zone(zone_name)
+        @zone_list[zone_name]
+      end
+
+      def software
+        @config.dnssec.software
+      end
+
+      def info
+        {
+          :software  => self.software
+        }
+      end
+
+      def flush_cache
+        fetch_zone_list()
+      end
+    end
+
+    class DNSSECZoneBase
+      attr_reader :name, :input_file, :output_file, :params
+
+      def initialize(config, name, input_file, output_file, params)
+        @config = config
+        @name = name
+        @input_file = input_file
+        @output_file = output_file
+        # backend-specific parameters, used for checks or generating useful info
+        @params = params
+      end
+
+      def info
+        {
+          :dnssec_input_file   => @input_file,
+          :dnssec_output_file  => @output_file
+        }
+      end
+
+      # methods a backend MUST implement
+      #   - resign
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood-mapmaker/dnssec/opendnssec.rb b/lib/cyborghood-mapmaker/dnssec/opendnssec.rb
new file mode 100644
index 0000000..b12482d
--- /dev/null
+++ b/lib/cyborghood-mapmaker/dnssec/opendnssec.rb
@@ -0,0 +1,59 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'cyborghood-mapmaker/dnssec/base'
+require "rexml/document"
+
+
+module CyborgHood
+  module MapMakerLand
+    class DNSSEC < DNSSECBase
+
+      protected
+
+      def fetch_zone_list
+        output = []
+        begin
+          IO.popen("sudo ods-control ksm zonelist export 2>/dev/null") do |fp|
+            output << fp.gets.chomp! until fp.eof?
+          end
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dnssec", "dnssec zone list could not be checked (I/O error)")
+        end
+
+        @zone_list = {}
+        doc = REXML::Document.new(output.join("\n"))
+        doc.elements.each('ZoneList/Zone') do |e|
+          zone_name = e.attributes['name']
+          @zone_list[zone_name] =
+            DNSSECZone.new(@config, zone_name,
+                           e.elements['Adapters/Input/File'].text,
+                           e.elements['Adapters/Output/File'].text,
+                           { :policy => e.elements['Policy'] })
+        end
+      end
+    end
+
+    class DNSSECZone < DNSSECZoneBase
+      def resign
+        system "sudo ods-control signer sign '#{@name}' >/dev/null"
+        raise CyberError.new(:unrecoverable, "services/dnssec", "zone resign failed") unless $?.success?
+      end
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood-mapmaker/interface/0_base.rb b/lib/cyborghood-mapmaker/interface/0_base.rb
index 575672b..9f9bd96 100644
--- a/lib/cyborghood-mapmaker/interface/0_base.rb
+++ b/lib/cyborghood-mapmaker/interface/0_base.rb
@@ -1,9 +1,12 @@
-store.dns = Services::DNS::System.new
+store.dns = bot.services.dns
 
 node 'Services', :dir => 'services'
 
 node 'Zones' do
-  zone_list = Proc.new{ store.dns.zones }
+  zone_list = Proc.new do
+    store.dns.flush_cache
+    store.dns.zones
+  end
 
   attr_search_node
 
diff --git a/lib/cyborghood-mapmaker/interface/_zone/0_base.rb b/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
index 8eb9fbb..bca9f76 100644
--- a/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
+++ b/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
@@ -1,17 +1,8 @@
-zone = Services::DNS::Zone.new(node_name)
+bot.services.dnssec.flush_cache
+zone = bot.services.zone(node_name)
 
 on_request do |request|
-  request.reply.results = {
-    :master  => zone.master?,
-    :signed  => zone.signed?,
-    :serial  => zone.serial_in_dns
-  }
-  if zone.master?
-    request.reply.results.merge!({
-                                   :serial_in_zone_file         => zone.serial_in_zone_file,
-                                   :serial_in_signed_zone_file  => zone.serial_in_signed_zone_file
-                                 })
-  end
+  request.reply.results = zone.info
 end
 
 node 'content' do
@@ -28,25 +19,10 @@ node 'content=' do
       return
     end
 
-    zone.content = content
-    if zone.changed?
-      check_result = zone.check
-      if check_result[:ok]
-        request.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
-          request.reply.errors << _("Zone serial is not superior to current serial.")
-        end
-      else
-        request.reply.errors = check_result[:errors]
-        zone.cancel_changes
-      end
-    else
-      request.reply.warnings << _("Zone is unmodified (same content)")
-                      zone.cancel_changes
+    begin
+      zone.content = content
+    rescue CyberError => e
+      request.reply.errors << e.message
     end
   end
 end
diff --git a/lib/cyborghood-mapmaker/interface/services/dns.rb b/lib/cyborghood-mapmaker/interface/services/dns.rb
index df7fbe2..30ddbca 100644
--- a/lib/cyborghood-mapmaker/interface/services/dns.rb
+++ b/lib/cyborghood-mapmaker/interface/services/dns.rb
@@ -1,8 +1,6 @@
 node 'DNS' do
   on_request do |request|
-    request.reply.results = {
-      :software => store.dns.type
-    }.merge(store.dns.status)
+    request.reply.results = store.dns.info
   end
 
   node 'check_config' do
diff --git a/lib/cyborghood-mapmaker/interface/services/dnssec.rb b/lib/cyborghood-mapmaker/interface/services/dnssec.rb
new file mode 100644
index 0000000..8ec89ca
--- /dev/null
+++ b/lib/cyborghood-mapmaker/interface/services/dnssec.rb
@@ -0,0 +1,5 @@
+node 'DNSSEC' do
+  on_request do |request|
+    request.reply.results = bot.services.dnssec.info
+  end
+end
diff --git a/lib/cyborghood-mapmaker/land.rb b/lib/cyborghood-mapmaker/land.rb
new file mode 100644
index 0000000..34f6c51
--- /dev/null
+++ b/lib/cyborghood-mapmaker/land.rb
@@ -0,0 +1,51 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# 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 MapMakerLand
+    def register_services(container)
+      container.namespace_define(:mapmaker_land) do |b|
+        b.dns do
+          require 'cyborghood-mapmaker/dns/' + b.config.dns.software
+          DNS.new(b.config)
+        end
+
+        b.dnssec do
+          require 'cyborghood-mapmaker/dnssec/' + b.config.dnssec.software
+          DNSSEC.new(b.config)
+        end
+
+        b.zone_editor do
+          require 'cyborghood-mapmaker/zone_editor'
+          ZoneEditor.new()
+        end
+
+        b.zone :model => :multiton do |c, p, name|
+          require 'cyborghood-mapmaker/zone'
+          Zone.new(c.config, c.dns, c.dnssec, c.zone_editor, name)
+        end
+      end
+    end
+
+    module_function :register_services
+  end
+end
diff --git a/lib/cyborghood-mapmaker/zone.rb b/lib/cyborghood-mapmaker/zone.rb
new file mode 100644
index 0000000..55585e9
--- /dev/null
+++ b/lib/cyborghood-mapmaker/zone.rb
@@ -0,0 +1,120 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+module CyborgHood
+  module MapMakerLand
+    class Zone
+      def initialize(config, dns, dnssec, zone_editor, name)
+        @config = config
+        @dns = dns
+        @dnssec = dnssec
+        @zone_editor = zone_editor
+        @name = name
+
+        @dns_zone = @dns.get_zone(@name)
+        @dns_zone_file = @dns.get_zone_file(@name)
+        @dnssec_zone = @dnssec.get_zone(@name)
+      end
+
+      def exists?
+        not @dns_zone.nil? and not @dns_zone_file.nil?
+      end
+
+      def info
+        i = {}
+        i.merge!(@dns_zone.info) unless @dns_zone.nil?
+        i.merge!(@dns_zone_file.info) unless @dns_zone_file.nil?
+        i.merge!(@dnssec_zone.info) unless @dnssec_zone.nil?
+        i
+      end
+
+      def content
+        @dns_zone_file.content
+      end
+
+      def content=(c)
+        @dns_zone_file.content = c
+
+        unless @dns_zone_file.changed?
+          raise CyberError.new(:unrecoverable, "zone", "zone did not change")
+        end
+
+        report = @dns_zone_file.check
+        unless report[:ok]
+          raise CyberError.new(:unrecoverable, "zone", "zone content is buggy: " + report[:errors].join(", "))
+        end
+
+        zone_signed = @dns_zone.signed?
+
+        # DNSSEC tools automatically increase serial
+        unless zone_signed
+          unless @dns_zone_file.parsed_content.serial > @dns_zone.serial
+            raise CyberError.new(:unrecoverable, "zone", "zone content serial is not superior to current serial")
+          end
+        end
+
+        @dns_zone_file.create_backup
+        @dns_zone_file.save
+        begin
+          if zone_signed
+            @dnssec_zone.resign
+          else
+            @dns_zone_file.activate
+          end
+        rescue
+          @dns_zone_file.import_from_backup
+          @dns_zone_file.save
+          raise CyberError.new(:unrecoverable, "zone", "zone activation failed, replacing old content")
+        end
+      end
+
+      def alter(recipe)
+        # TODO: use a ZoneEditor to handle the recipe and save the result
+        # TODO: put as many things in common with content=()
+        # TODO: auto-increase the serial if not already done in the recipe and the zone is not signed
+      end
+
+      def check
+        report = {
+          :errors    => [],
+          :warnings  => []
+        }
+
+        if @dns_zone.signed?
+          if @dns_zone_file.filename != @dnssec_zone.input_file
+              report[:errors] << _("DNS and DNSSEC original zone files do not match")
+          end
+          if @dns_zone_file.filename_signed != @dnssec_zone.output_file
+              report[:errors] << _("DNS and DNSSEC signed zone files do not match")
+          end
+        end
+
+        zone_file_serial = @dns_zone.signed? ? @dns_zone_file.parsed_signed_content.serial :
+          @dns_zone_file.parsed_content.serial
+        if zone_file_serial != @dns_zone.serial
+          report[:warnings] << _("The zone serial does not match the one in the zone file")
+        end
+
+        # TODO: more checks
+
+        report
+      end
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood-mapmaker/zone_content.rb b/lib/cyborghood-mapmaker/zone_content.rb
new file mode 100644
index 0000000..be8e5ca
--- /dev/null
+++ b/lib/cyborghood-mapmaker/zone_content.rb
@@ -0,0 +1,92 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'dnsruby'
+require 'tempfile'
+
+
+module CyborgHood
+  module MapMakerLand
+    class ZoneContentBase
+      def initialize(name)
+        @name = name
+      end
+
+      def soa
+        find_rr('SOA').first
+      end
+
+      def serial
+        s = self.soa
+        s ? s.serial : nil
+      end
+
+      def signed?
+        not find_rr('DNSKEY').empty?
+      end
+
+      # methods a backend MUST implement
+      #   - find_rr
+    end
+
+    class ZoneContent < ZoneContentBase
+      def initialize(name)
+        super
+
+        @reader = Dnsruby::ZoneReader.new(@name)
+        @zone = []
+      end
+
+      def content
+        @zone.collect{|rr| rr.to_s }.join("\n")
+      end
+
+      def content=(str)
+        begin
+          temp_file = Tempfile.new(@name)
+          temp_file.write(str)
+          temp_file.close
+        rescue
+          raise CyberError.new(:unrecoverable, "services/dns", "could not save temporary zone")
+        end
+
+        import_from_file(temp_file.path)
+
+        temp_file.close!
+      end
+
+      def import_from_file(file)
+        @zone = @reader.process_file(file) || []
+      end
+
+      def to_s
+        self.content
+      end
+
+      def empty?
+        @zone.empty?
+      end
+
+      def find_rr(rr_type)
+        @zone.select{|rr| rr.name.to_s == @name and rr.type == rr_type }
+      end
+
+      # TODO: methods to add/replace RRs
+    end
+  end
+end
diff --git a/lib/cyborghood-mapmaker/zone_editor.rb b/lib/cyborghood-mapmaker/zone_editor.rb
new file mode 100644
index 0000000..1f332f1
--- /dev/null
+++ b/lib/cyborghood-mapmaker/zone_editor.rb
@@ -0,0 +1,27 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'cyborghood-mapmaker/zone_content'
+
+
+module CyborgHood
+  module MapMakerLand
+    class ZoneEditor
+    end
+  end # MapMakerLand
+end
diff --git a/lib/cyborghood.rb b/lib/cyborghood.rb
index 55c49a2..e728600 100644
--- a/lib/cyborghood.rb
+++ b/lib/cyborghood.rb
@@ -19,6 +19,7 @@
 $KCODE = 'UTF8'
 require 'jcode'
 require 'pp'
+require 'needle'
 require 'cyborghood/base/config'
 require 'cyborghood/base/info'
 require 'cyborghood/base/lang_additions'
@@ -26,8 +27,19 @@ require 'cyborghood/base/logger'
 require 'cyborghood/base/language'
 require 'cyborghood/base/exceptions'
 
+
 module CyborgHood
   include I18nTranslation
   ENV['LC_ALL'] = "C"
   bindtextdomain("cyborghood", {:path => Config::L10N_DIR, :charset => "UTF-8"})
+
+  def register_services(container)
+    container.define do |b|
+      b.config do
+        Config.instance
+      end
+    end
+  end
+
+  module_function :register_services
 end
diff --git a/lib/cyborghood/cyborg.rb b/lib/cyborghood/cyborg.rb
index 15c3905..cdc8a29 100644
--- a/lib/cyborghood/cyborg.rb
+++ b/lib/cyborghood/cyborg.rb
@@ -26,11 +26,12 @@ module CyborgHood
     include I18nTranslation
     bindtextdomain("cyborghood", {:path => Config::L10N_DIR, :charset => "UTF-8"})
 
-    attr_reader :name
+    attr_reader :name, :services
 
-    def initialize
-      @name = self.class.name.split("::").last
+    def initialize(services = nil)
+      @services = services
 
+      @name = self.class.name.split("::").last
       @config = Config.instance
 
       # setup logs
