commit ca79e27e4f538a8ddfbfccb3342ea3c1fe0167c6
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Aug 15 05:21:46 2010 +0200

    [evol] reworked DNS classes (refs #31)

diff --git a/bin/mapmaker b/bin/mapmaker
index 3dfa443..09bd024 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -57,7 +57,7 @@ module CyborgHood
       include CyborgServerInterface
       include CyborgServerDefaultInterface
 
-      class DNS < Services::DNS
+      class DNS < Services::DNS::System
         include CyborgServerInterface
 
         export_parent_methods
@@ -71,7 +71,7 @@ module CyborgHood
           include CyborgServerInterface
 
           def initialize
-            @dns = Services::DNS.new
+            @dns = Services::DNS::System.new
           end
 
           def api_methods
@@ -86,7 +86,7 @@ module CyborgHood
             end
           end
 
-          class Zone < Services::Zone
+          class Zone < Services::DNS::Zone
             include CyborgServerEmbededInterface
           end
         end
diff --git a/lib/cyborghood/cyborg/interface.rb b/lib/cyborghood/cyborg/interface.rb
index 92c497a..135a4d5 100644
--- a/lib/cyborghood/cyborg/interface.rb
+++ b/lib/cyborghood/cyborg/interface.rb
@@ -157,6 +157,8 @@ module CyborgHood
         # preliminary outgoing message handling
         action.call(*formated_data).to_yaml
       rescue
+        logger.debug "node action error message: " + $!
+        logger.debug "node action error backtrace: " + $!.backtrace.join("\n")
         return "550 method call failed: " + $!
       end
     end
diff --git a/lib/cyborghood/cyborg/server.rb b/lib/cyborghood/cyborg/server.rb
index 08c1beb..ba95d62 100644
--- a/lib/cyborghood/cyborg/server.rb
+++ b/lib/cyborghood/cyborg/server.rb
@@ -126,13 +126,10 @@ module CyborgHood
           end
           cmd = $1
           flags = $2 || ""
-pp "GROK"
-pp cmd
-pp flags
 
           if flags.index '?'
             send_line "250+ ok"
-            send_line({'exists?' => @interface.has_node?(cmd)}.to_yaml)
+            send_line({'exists' => @interface.has_node?(cmd)}.to_yaml)
             return
           end
 
diff --git a/lib/cyborghood/services/dns.rb b/lib/cyborghood/services/dns.rb
index 50620ee..71d4a47 100644
--- a/lib/cyborghood/services/dns.rb
+++ b/lib/cyborghood/services/dns.rb
@@ -16,6 +16,8 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #++
 
+require 'digest/md5'
+require 'tempfile'
 require 'fileutils'
 
 # ensure we can find the needed programs (should be handled somewhere else)
@@ -23,137 +25,224 @@ ENV['PATH'] = (ENV['PATH'].split(":") + ["/sbin", "/usr/sbin", "/usr/local/sbin"
 
 module CyborgHood
   module Services
-    class DNS
-      def initialize
-        @config = Config.instance
+    module DNS
+      class System
+        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
+          @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?
+        def zones
           Dir.glob(@zone_files_pattern).collect do |file|
             $1 if file =~ @zone_files_regex
           end
-        else
+        end
+
+        def [](zone)
+          return unless zones.include?(zone)
+
           Zone.new(zone)
         end
+
+        def check_zone_file(check_type)
+          case @config.dns.software
+          when 'bind'
+            output = []
+            begin
+              IO.popen("named-checkconf '#{@zone}' #{filename}") 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}.to_ostruct
+            else
+              return {:ok => false, :errors => output}.to_ostruct
+            end
+          end
+        end
       end
-    end
 
-    class Zone
-      attr_reader :zone
-      attr_accessor :content
+      class Zone
+        def initialize(zone)
+          @zone = zone
 
-      def initialize(zone)
-        @zone = zone
+          @config = Config.instance
+          @content = nil
+          @temp_file = nil
 
-        @config = Config.instance
-        @content = nil
+          @filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
 
-        @filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
-      end
+          at_exit {cleanup_temp}
+        end
 
-      def read_zone
-        begin
-          @content = File.read(@filename)
-        rescue
-          raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' cannot be read (nonexistent or lack of permission)")
+        def content
+          read_zone(@filename) if @content.nil?
+          @content
         end
-      end
 
-      def serial
-        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 (I/O error)")
+        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
-          raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found (#{output.first})") unless $?.success?
-
-          if $? == 0
-            serial = nil
-            output.each do |l|
-              if l =~ /: loaded serial (\d+)$/
-                serial = $1
-                break
-              end
-            end
 
-            if serial
-              serial
-            else
-              raise CyberError.new(:unrecoverable, "services/dns", "no serial returned")
-            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 serial(force_real = false)
+          r = check_zone_file('none', force_real = false)
+          if r.ok
+            r.serial
           else
-            nil
+            raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found (#{r.errors.first})")
           end
         end
-      end
 
-      def check_zone_file(filename)
-        case @config.dns.software
-        when 'bind'
-          output = []
+        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
+          system "sudo rndc reload '#{@zone}' >/dev/null"
+          raise CyberError.new(:unrecoverable, "services/dns", "zone activation failed") unless $?.success?
+        end
+
+        protected
+
+        def backup_filename
+          @filename + ".ch-backup"
+        end
+
+        def save_to_temp
+          return unless temp_file.nil?
+
           begin
-            IO.popen("named-checkzone '#{@zone}' #{filename}") do |fp|
-              output << fp.gets.chomp! until fp.eof?
-            end
+            @temp_file = Tempfile.new(@zone)
+            @temp_file.write(@content)
+            @temp_file.close
           rescue
-            raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' could not be checked")
+            raise CyberError.new(:unrecoverable, "services/dns", "could not save temporary zone")
           end
+        end
 
-          if $? == 0
-            serial = nil
-            messages = []
-            output.each do |l|
-              next if l == "OK"
-              if l =~ /: loaded serial (\d+)$/
-                serial = $1
-                next
-              end
-              messages << l
-            end
+        def cleanup_temp
+          return if @temp_file.nil?
 
-            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
+          @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
-      end
 
-      def write_zone_from_file(new_zone_filename)
-        # create backup
-        FileUtils.cp(@filename, backup_zone_filename())
+        def update_hash
+          @content_hash = Digest::MD5.hexdigest(@content)
+        end
 
-        FileUtils.cp(new_zone_filename, @filename)
-      end
+        def read_zone(filename)
+          begin
+            @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)")
+          end
+        end
 
-      def replace_zone_with_backup
-        if File.exists?(backup_zone_filename())
-          FileUtils.cp(backup_zone_filename(), @filename)
-        else
-          raise CyberError.new(:unrecoverable, "services/dns", "no zone backup file found")
+        def write_zone(filename)
+          begin
+            File.open(filename, "w") do |fp|
+              fp.print @content
+            end
+          rescue
+            raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' cannot be written from '#{filename}' (I/O error or lack of permission)")
+          end
         end
-      end
 
-      def reload_zone
-        system "sudo rndc reload '#{@zone}' >/dev/null"
-      end
+        def check_zone_file(check_type, force_real = false)
+          filename = force_real ? @filename : current_filename
 
-      private
+          case @config.dns.software
+          when 'bind'
+            output = []
+            begin
+              IO.popen("named-checkzone -i #{check_type} '#{@zone}' #{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)")
+            end
+
+            if $?.success?
+              serial = nil
+              messages = []
+              output.each do |l|
+                next if l == "OK"
+                if l =~ /: loaded serial (\d+)$/
+                  serial = $1
+                  next
+                end
+                messages << l
+              end
 
-      def backup_zone_filename
-        @filename + ".ch-backup"
+              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
