commit 66a0224083dc5772259332902df4aab946afa4bd
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Feb 27 13:55:19 2011 +0100

    [evol] DNS API rework §1

diff --git a/bin/mapmaker b/bin/mapmaker
index f2b11ce..4c5c5e6 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -22,7 +22,7 @@
 $: << File.join(File.dirname(__FILE__), "..", "lib")
 
 require 'cyborghood/cyborg'
-require 'cyborghood/services/dns'
+require 'cyborghood-mapmaker/dns'
 
 
 module CyborgHood
diff --git a/bin/test_client b/bin/test_client
index 8eab7c0..749c4c9 100755
--- a/bin/test_client
+++ b/bin/test_client
@@ -39,14 +39,16 @@ module CyborgHood
         task "compare stuff" do
           ask "MapMaker", :info1, "/_cyborg_"
           ask "Librarian", :info2, "/_cyborg_"
-          ask "MapMaker", :zones, "/DNS/Zones"
+          ask "MapMaker", :zones, "/Zones"
           #ask "MapMaker", :wanted_failure, "/prout"
-          ask "MapMaker", :zone_mp, "/DNS/Zones/milkypond.org"
-          ask "MapMaker", :search, "/DNS/Zones/?"
-          ask "MapMaker", :search_master, "/DNS/Zones/?", {:master => true}
-          ask "MapMaker", :search_slave, "/DNS/Zones/?", {:master => false}
-          know? "MapMaker", :k1, "/DNS/Zones"
-          know? "MapMaker", :k2, "/prout"
+          ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
+          ask "MapMaker", :dns, "/Services/DNS"
+          ask "MapMaker", :dns_check, "/Services/DNS/check_config"
+          #ask "MapMaker", :search, "/Zones/?"
+          #ask "MapMaker", :search_master, "/Zones/?", {:master => true}
+          #ask "MapMaker", :search_slave, "/Zones/?", {:master => false}
+          #know? "MapMaker", :k1, "/Zones"
+          #know? "MapMaker", :k2, "/prout"
           on_error do
             puts "PLOUF"
             pp errors
diff --git a/lib/cyborghood-mapmaker/dns.rb b/lib/cyborghood-mapmaker/dns.rb
new file mode 100644
index 0000000..c4b55e7
--- /dev/null
+++ b/lib/cyborghood-mapmaker/dns.rb
@@ -0,0 +1,353 @@
+#--
+# 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/interface/0_base.rb b/lib/cyborghood-mapmaker/interface/0_base.rb
index 206ff7c..575672b 100644
--- a/lib/cyborghood-mapmaker/interface/0_base.rb
+++ b/lib/cyborghood-mapmaker/interface/0_base.rb
@@ -1 +1,11 @@
 store.dns = Services::DNS::System.new
+
+node 'Services', :dir => 'services'
+
+node 'Zones' do
+  zone_list = Proc.new{ store.dns.zones }
+
+  attr_search_node
+
+  node zone_list, :dir => '_zone'
+end
diff --git a/lib/cyborghood-mapmaker/interface/_zone/0_base.rb b/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
index e208b73..8eb9fbb 100644
--- a/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
+++ b/lib/cyborghood-mapmaker/interface/_zone/0_base.rb
@@ -14,13 +14,13 @@ on_request do |request|
   end
 end
 
-node "content" do
+node 'content' do
   on_request do |request|
     request.reply.results = {:content => zone.content}
   end
 end
 
-node "content=" do
+node 'content=' do
   on_request do |request|
     content = request.args.shift
     if content.empty?
diff --git a/lib/cyborghood-mapmaker/interface/dns.rb b/lib/cyborghood-mapmaker/interface/dns.rb
deleted file mode 100644
index 7a7b3e3..0000000
--- a/lib/cyborghood-mapmaker/interface/dns.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-node "DNS" do
-  on_request do |request|
-    request.reply.results = {
-      :type => store.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{ store.dns.zones }
-
-    attr_search_node
-
-    node zone_list, :dir => '_zone'
-  end
-end
diff --git a/lib/cyborghood-mapmaker/interface/services/dns.rb b/lib/cyborghood-mapmaker/interface/services/dns.rb
new file mode 100644
index 0000000..df7fbe2
--- /dev/null
+++ b/lib/cyborghood-mapmaker/interface/services/dns.rb
@@ -0,0 +1,13 @@
+node 'DNS' do
+  on_request do |request|
+    request.reply.results = {
+      :software => store.dns.type
+    }.merge(store.dns.status)
+  end
+
+  node 'check_config' do
+    on_request do |request|
+      request.reply.results = store.dns.check_config
+    end
+  end
+end
diff --git a/lib/cyborghood/command_runner.rb b/lib/cyborghood/command_runner.rb
index f6b2986..a43ff9f 100644
--- a/lib/cyborghood/command_runner.rb
+++ b/lib/cyborghood/command_runner.rb
@@ -21,7 +21,6 @@ require 'tempfile'
 require 'shellwords'
 require 'cyborghood/order'
 require 'cyborghood/objects'
-require 'cyborghood/services/dns'
 
 
 module CyborgHood
diff --git a/lib/cyborghood/services/dns.rb b/lib/cyborghood/services/dns.rb
deleted file mode 100644
index e13e069..0000000
--- a/lib/cyborghood/services/dns.rb
+++ /dev/null
@@ -1,322 +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}.to_ostruct
-            else
-              return {:ok => false, :errors => output}.to_ostruct
-            end
-          end
-        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
