commit 157c68c922e40f82f4bf567b4139c48cb7eda82a
Author: Marc Dequenes <duck@Orfeo.duckcorp.org>
Date:   Sat Feb 21 21:48:26 2009 +0100

    [evol] implement changing a zone

diff --git a/lib/cyborghood/services/dns.rb b/lib/cyborghood/services/dns.rb
index 789d1c7..973e73c 100644
--- a/lib/cyborghood/services/dns.rb
+++ b/lib/cyborghood/services/dns.rb
@@ -1,22 +1,117 @@
+require 'fileutils'
+
 module CyborgHood
   module Services
     class DNS
+      attr_reader :zone
+      attr_accessor :content
+
       def initialize(zone)
         @zone = zone
 
         @config = Config.instance
+        @content = nil
+
+        # 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
-        file = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
-        #  TODO: should be checked at startup time
-        raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: pattern is constant") if file == @config.dns.master_zone_pattern
+        filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
+        # TODO: should be checked at startup time
+        raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: pattern is constant") if filename == @config.dns.master_zone_pattern
+
         begin
-          File.read(file)
+          @content = File.read(filename)
         rescue
           raise CyberError.new(:unrecoverable, "services/dns", "zone '#{@zone}' cannot be read (nonexistent or lack of permission)")
         end
       end
+
+      def serial
+        filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
+        # TODO: should be checked at startup time
+        raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: pattern is constant") if filename == @config.dns.master_zone_pattern
+
+        case @config.dns.nameserver || :bind
+        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")
+          end
+
+          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
+          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
+          output = []
+          begin
+            IO.popen("named-checkzone '#{@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")
+          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
+
+            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
+        else
+          # TODO: should be checked at startup time
+          raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: unknown nameserver")
+        end
+      end
+
+      def write_zone_from_file(new_zone_filename)
+        filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
+        # TODO: should be checked at startup time
+        raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: pattern is constant") if filename == @config.dns.master_zone_pattern
+
+        FileUtils.cp(new_zone_filename, filename)
+      end
     end
   end
 end
diff --git a/postman b/postman
index 67fb4a2..63fda9b 100755
--- a/postman
+++ b/postman
@@ -5,8 +5,7 @@
 $: << "./lib"
 
 #require 'socket'
-#require 'fileutils'
-#require 'tempfile'
+require 'tempfile'
 require 'shellwords'
 require 'cyborghood/imap'
 require 'cyborghood/mail'
@@ -74,7 +73,83 @@ class CommandParser
           ok = false
         end
       when "SET"
-        result.message = "Command not yet implemented."
+        case cmdline.shift.upcase
+        when "ZONE"
+          zone = cmdline.shift.downcase
+          dom = CyborgHood::DnsDomain.new(zone)
+          logger.info "User requesting zone content for '#{zone}'"
+          if dom.hosted?
+            if dom.managed_by? user
+              logger.info "User is manager of the zone"
+              srv_dns = CyborgHood::Services::DNS.new(zone)
+
+              content_ref = cmdline.shift.downcase
+              if content_ref =~ /^@(\d+)$/
+                part_ref = $1.to_i
+                if (1..refs.size).include? part_ref
+                  part = refs[part_ref]
+                  if part.content_type == "text/plain"
+                    f = Tempfile.new(zone)
+                    f.write(part.body)
+                    f.close
+                    logger.debug "Created temporary zone file '#{f.path}'"
+
+                    srv_dns = CyborgHood::Services::DNS.new(zone)
+                    current_serial = srv_dns.serial
+                    logger.debug "Current serial: #{current_serial}"
+
+                    result = srv_dns.check_zone_file(f.path)
+                    if result.ok
+                      logger.debug "New serial: #{result.serial}"
+                      # allow new serial or missing serial (to allow creating a new zone or replacing a broken zone)
+                      if current_serial.nil? or result.serial > current_serial
+                        begin
+                          srv_dns.write_zone_from_file(f.path)
+                          logger.info "zone changed"
+                        rescue
+                          logger.debug "Writing zone file failed"
+                          raise
+                        ensure
+                          f.close!
+                        end
+                      else
+                        logger.info "zone serial is not superior to current serial"
+                        result.message = "Zone serial is not superior to current serial."
+                        f.close!
+                        return result
+                      end
+                    else
+                      logger.info "new zone file is invalid"
+                      result.message = "invalid zone data"
+                      f.close!
+                      return result
+                    end
+
+                    f.close!
+                  else
+                    logger.info "attachment for zone is not plain text"
+                    result.message = "Attachment has wrong type."
+                    return result
+                  end
+                else
+                  logger.info "attachement for zone not found"
+                  result.message = "Attachment number not found."
+                  return result
+                end
+              else
+                ok = false
+              end
+            else
+              logger.info "User is not allowed to manage the zone"
+              result.message = "You are not allowed to manage this zone."
+            end
+          else
+            logger.info "Zone not hosted"
+            result.message "This zone is not hosted here."
+          end
+        else
+          ok = false
+        end
       else
         ok = false
       end
