diff --git a/generators/puppet/bindg5k.rb b/generators/puppet/bindg5k.rb
index e471f33f90cf7455ccd4188f1b0235b5dcdb9204..6c1c9b748eae262d03b7f5f2baf40392da53d0a2 100644
--- a/generators/puppet/bindg5k.rb
+++ b/generators/puppet/bindg5k.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/ruby
+# coding: utf-8
 
 if RUBY_VERSION < "2.1"
   puts "This script requires ruby >= 2.1"
@@ -12,15 +13,17 @@ require 'erb'
 require 'pathname'
 require 'fileutils'
 require 'optparse'
+require 'dns/zone'
 require_relative '../lib/input_loader'
 
 input_data_dir = "../../input/grid5000/"
 
 refapi = load_yaml_file_hierarchy(File.expand_path(input_data_dir, File.dirname(__FILE__)))
 
-options = {}
-options[:sites] = %w{grenoble lille luxembourg lyon nancy nantes rennes sophia}
-options[:output_dir] = "/tmp/puppet-repo"
+$options = {}
+$options[:sites] = %w{grenoble lille luxembourg lyon nancy nantes rennes sophia}
+$options[:output_dir] = "/tmp/puppet-repo"
+$options[:verbose] = false
 
 OptionParser.new do |opts|
   opts.banner = "Usage: bindg5k.rb [options]"
@@ -28,16 +31,20 @@ OptionParser.new do |opts|
   opts.separator ""
   opts.separator "Example: ruby bindg5k.rb -s nancy -o /tmp/puppet-repo"
 
-  opts.on('-o', '--output-dir dir', String, 'Select the puppet repo path', "Default: " + options[:output_dir]) do |d|
-    options[:output_dir] = d
+  opts.on('-o', '--output-dir dir', String, 'Select the puppet repo path', "Default: " + $options[:output_dir]) do |d|
+    $options[:output_dir] = d
   end
 
   opts.separator ""
   opts.separator "Filters:"
 
-  opts.on('-s', '--sites a,b,c', Array, 'Select site(s)', "Default: " + options[:sites].join(", ")) do |s|
-    raise "Wrong argument for -s option." unless (s - options[:sites]).empty?
-    options[:sites] = s
+  opts.on('-s', '--sites a,b,c', Array, 'Select site(s)', "Default: " + $options[:sites].join(", ")) do |s|
+    raise "Wrong argument for -s option." unless (s - $options[:sites]).empty?
+    $options[:sites] = s
+  end
+
+  opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+    $options[:verbose] = true
   end
 
   opts.on_tail("-h", "--help", "Show this message") do
@@ -46,269 +53,440 @@ OptionParser.new do |opts|
   end
 end.parse!
 
-$puppet_header = "; This file was generated by reference-repository.git/generators/puppet/bindg5k.rb\n; Do not edit this file by hand. Your changes will be overwritten.\n\n"
-
-def write_site_zones(site_uid, site, zones_dir, dns_entries)
-
-  site_zone_content = "\n"
-
-  dns_entries.each { |type, entries|
-
-    next if entries["addr"].empty?
-
-    file_name = site_uid + "-" + type + ".db"
-    site_zone_content += "$INCLUDE /etc/bind/zones/#{site_uid}/#{file_name}\n"
- 
-    output_file = File.join(zones_dir, file_name)
-    FileUtils.mkdir_p(File.dirname(output_file))
-    #Don't write bond header here because those files will be included in a file that already  defines it.
-    File.write(output_file, $puppet_header + entries["addr"].join("\n") + "\n\n", node: 'wa+')
-    if (! entries["cnames"].empty?)
-      File.write(output_file, "\n; CNAMES\n" + entries["cnames"].join("\n") + "\n\n", mode: 'a')
-    end
-    $written_files << output_file
-  }
-
-  # DNS (/modules/bindg5k/files/zones/nancy.db)
-  manual = site_uid + '-manual.db'
-  if File.exist?(File.join(zones_dir, manual)) # add include statement
-    site_zone_content += "$INCLUDE /etc/bind/zones/#{site_uid}/#{manual}\n\n"
+#Prettier aligned dump of records
+class DNS::Zone::RR::A
+  def dump
+    max_pad = 30
+    return "#{@label.ljust(max_pad)} IN A #{' ' * 6 + @address}"
   end
-
-  output_file = site_uid + '.db'
-  header = ERB.new(File.read(File.expand_path('templates/bind-header.erb', File.dirname(__FILE__)))).result(binding)
-  FileUtils.mkdir_p(File.dirname(File.join(zones_dir, output_file)))
-  File.write(File.join(zones_dir, output_file), $puppet_header + header + "\n" + site_zone_content)
-  $written_files << File.join(zones_dir, output_file).to_s
 end
 
-def write_reverse_site_zones(site_uid, zones_dir, reverse_entries)
-
-  #Reverse DNS (/modules/bindg5k/files/zones/reverse-*db)
-  reverse_entries.each { |output_file, output|
-    header = ERB.new(File.read(File.expand_path('templates/bind-header.erb', File.dirname(__FILE__)))).result(binding) # do not move outside of the loop (it uses the output_file variable)
-    manual = output_file.sub('.db', '') + '-manual.db'
-    output.unshift("$INCLUDE /etc/bind/zones/#{site_uid}/#{manual}") if File.exist?(File.join(zones_dir, manual))
-    FileUtils.mkdir_p(File.dirname(File.join(zones_dir, output_file)))
-    File.write(File.join(zones_dir, output_file), $puppet_header + header + output.join("\n") + "\n")
-    $written_files << File.join(zones_dir, output_file).to_s
-  }
-
-  #Create reverse-*.db files for each reverse-*-manual.db that do not have (yet) a corresponding file.
-  Dir.glob(File.join("#{zones_dir}", "reverse-*-manual.db")).each { |reverse_manual_file|
-    reverse_manual_file = File.basename(reverse_manual_file);
-    output_file = reverse_manual_file.sub("-manual.db", ".db")
-    next if File.exists?(File.join(zones_dir, output_file))
-    puts "Including orphan reverse manual file: #{reverse_manual_file}"
-    header = ERB.new(File.read(File.expand_path('templates/bind-header.erb', File.dirname(__FILE__)))).result(binding)
-    content = "$INCLUDE /etc/bind/zones/#{site_uid}/#{reverse_manual_file}"
-    FileUtils.mkdir_p(File.dirname(File.join(zones_dir, output_file)))
-    File.write(File.join(zones_dir, output_file), $puppet_header + header + "\n" + content + "\n")
-    $written_files << File.join(zones_dir, output_file).to_s
-  }
+class DNS::Zone::RR::CNAME
+  def dump
+    max_pad = 30
+    return @label.ljust(max_pad) + " IN CNAME " + ' ' * 6 + @domainname
+  end
 end
 
-# files/global/conf/global-nancy.conf
-# files/site/conf/global-nancy.conf
-#zones_dir needed in erb
-def write_global_site_conf(site_uid, dest_dir, zones_dir)
-
-  ['global', 'site'].each { |dir|
-    conf_file = File.join(dest_dir, "#{dir}/conf", "global-#{site_uid}.conf")
-    FileUtils.mkdir_p(File.dirname(conf_file))
-    conf_content = ERB.new(File.read(File.expand_path('templates/bind-global-site.conf.erb', File.dirname(__FILE__)))).result(binding)
-    File.write(conf_file, conf_content)
-  }
+class DNS::Zone::RR::NS
+  def dump
+    max_pad = 30
+    return @label.ljust(max_pad) + " IN NS " + ' ' * 6 + @nameserver
+  end
 end
 
-# Create a dns entry
-def print_entry(entry)
-  max_pad = 30 #Sane default for maximum hostname length
-  addr = []
-  cnames = []
-  hostshort = entry[:uid]
-  hostshort += "-" + entry[:node_uid].to_s if entry[:node_uid]
-  ip        = entry[:ip]
-  hostname  = entry[:hostsuffix] ? hostshort + entry[:hostsuffix] : hostshort # graoully-$-eth0
-
-  if entry[:cnamesuffix]
-    hostalias = hostshort + entry[:cnamesuffix]
-    addr << "#{hostalias.ljust(max_pad)} IN A #{' ' * 6 + ip}"
-    cnames << "#{hostname.ljust(max_pad)} IN CNAME #{' ' * 6 + hostalias}"
-    #If there are cnames defined, point cnames to defined hostname instead
-    hostname = hostalias
-  else
-    addr << "#{hostname.ljust(max_pad)} IN A #{' ' * 6 + ip}"
+class DNS::Zone::RR::MX
+  def dump
+    max_pad = 30
+    return @label.ljust(max_pad) + " MX " + @priority.to_s + " " * 6 + @exchange
   end
-  if entry[:cnames]
-    entry[:cnames].each{ |cname|
-      cnames << "#{cname.ljust(max_pad)} IN CNAME #{' ' * 6 + hostname}"
-    }
+end
+
+class DNS::Zone::RR::SOA
+  #Keep the previous version of soa format
+  def dump
+    content = "@" + " " * 23 + "IN      SOA     "
+    content += @nameserver + " "
+    content += @email + " (\n"
+    content += " " * 32 + @serial.to_s + " ; serial (YYYYMMDDSS)\n"
+    content += " " * 32 + @refresh_ttl + " " * 9 + "; refresh\n"
+    content += " " * 32 + @retry_ttl + " " * 9 + "; retry\n"
+    content += " " * 32 + @expiry_ttl + " " * 9 + "; expire\n"
+    content += " " * 32 + @minimum_ttl + ")" + " " * 8 + "; negative caching\n"
+    return content
   end
-  return addr, cnames
 end
 
-# Examples: reverse-64.16.172.db
-# 96                                 IN      PTR     opensm.nancy.grid5000.fr.
-def print_reverse_entry(site_uid, entry)
-  hostshort = entry[:uid]
-  hostshort += "-" + entry[:node_uid].to_s if entry[:node_uid]
-  ipsuffix = entry[:ip].split(".")[3]
-  file = "#{entry[:ip].split('.')[0..2].reverse.join('.')}" # 70.16.172
+class DNS::Zone
+
+  attr_accessor :file_path
+  attr_accessor :site_uid
+  attr_accessor :header
+  attr_accessor :soa
+  attr_accessor :ns
+  attr_accessor :mx
+  attr_accessor :at
+  attr_accessor :include
+
+  def get_header
+    content = "; This file was generated by reference-repository.git/generators/puppet/bindg5k.rb\n; Do not edit this file by hand. Your changes will be overwritten.\n"
+    if header
+      content += "$TTL 3h\n"
+      content += soa.dump + "\n"
+      content += ns.dump + "\n"
+      if at
+        content += at.dump + "\n"
+      end
+      content += mx.dump + "\n"
+    end
+    return content
+  end
 
-  if entry[:cnamesuffix].nil?
-    hostname  = entry[:hostsuffix] ? hostshort + entry[:hostsuffix] : hostshort # graoully-$-eth0
-  else
-    hostname  = hostshort + entry[:cnamesuffix]
+  #Re-define Zone dump
+  def dump
+    last_type = ""
+    content = []
+    if @include
+      content << "\n" + @include
+    end
+    @records.each { |record|
+      if record.type != last_type
+        content << "\n; #{record.type} records"
+      end
+      content << record.dump
+      last_type = record.type
+    }
+    return get_header() + content.join("\n") << "\n"
   end
-  return ["reverse-#{file}.db", "#{ipsuffix} IN PTR #{hostname}.#{site_uid}.grid5000.fr."]
 end
 
-def get_servers_entries(site)
-  entries = []
-
+def get_servers_records(site)
+  records = []
   site['servers'].sort.each { |server_uid, server|
 
     next if server['network_adapters'].nil?
 
     server['network_adapters'].each { |net_uid, net|
-
       next if net['ip'].nil?
 
-      new_entry = {
-        :uid         => server_uid,
-        :hostsuffix  => net_uid != 'default' ? "-#{net_uid}" : '',
-        :ip          => net['ip']
-      }
+      new_record = DNS::Zone::RR::A.new
+      new_record.address = net['ip']
+      new_record.label = server_uid
+      new_record.label += "-#{net_uid}" if net_uid != 'default'
+      records << new_record
+
       if server['alias']
-        new_entry[:cnames] = server['alias'].reject{ |cname| cname.include?('.') }.map{ |cname|
-          net_uid == 'default' ? cname : "#{cname}-#{net_uid}"
+        server['alias'].reject{ |cname| cname.include?('.') }.each{ |cname|
+          cname_record = DNS::Zone::RR::CNAME.new
+          cname_record.label = cname
+          cname_record.label += "-#{net_uid}" if net_uid != 'default'
+          cname_record.domainname = server_uid
+          cname_record.domainname += "-#{net_uid}" if net_uid != 'default'
+          records << cname_record
         }
       end
-      entries << new_entry
     }
   }
-
-  return entries
+  return records
 end
 
-def get_pdus_entries(site)
-  entries = []
+def get_pdus_records(site)
+  records = []
 
   site['pdus'].sort.each { |pdu_uid, pdu|
 
     next unless pdu['ip']
 
-    new_entry = {
-      :uid     => pdu_uid,
-      :ip      => pdu['ip']
-    }
-    entries << new_entry
+    new_record = DNS::Zone::RR::A.new
+    new_record.address = pdu['ip']
+    new_record.label = pdu_uid
+    records << new_record
   }
-  return entries
+  return records
 end
 
-def get_networks_entries(site, key)
-  entries = []
+def get_networks_records(site, key)
+  records = []
 
   site[key].sort.each { |uid, node|
     if node['network_adapters'].nil?
-      puts "Warning: no network_adapters for #{uid}" 
+      puts "Warning: no network_adapters for #{uid}"
       next
     end
 
-    eth_net_uid = node['network_adapters'].select{ |u, h| h['mounted'] && /^eth[0-9]$/.match(u) } # eth* interfaces
+    eth_net_uid = node['network_adapters'].select{ |u, h| h['mounted'] && /^eth[0-9]$/.match(u) } # eth* interfaces 
     node['network_adapters'].each { |net_uid, net_hash|
-      hostsuffix = nil
       if ! eth_net_uid.include?(net_uid) && node['network_adapters'].size > 1
         hostsuffix = "-#{net_uid}"
+      else
+        hostsuffix = ''
       end
-      new_entry = {
-        :uid         => uid,
-        :hostsuffix  => hostsuffix, # cacahuete vs. cacahuete-eth0
-        :ip          => net_hash['ip'],
-      }
-      entries << new_entry
+      new_record = DNS::Zone::RR::A.new
+      new_record.address = net_hash['ip']
+      new_record.label = uid + hostsuffix
+      records << new_record
     }
   }
-  return entries
+  return records
 end
 
-def get_node_entries(cluster_uid, node_uid, network_adapters)
-
-  entries = {}
+def get_node_records(cluster_uid, node_uid, network_adapters)
 
+  records = []
+ 
   network_adapters.each { |net_uid, net_hash|
 
     next unless net_hash['ip']
 
     node_id = node_uid.to_s.split(/(\d+)/)[1].to_i # node number
-    ip = net_hash['ip']
 
-    hostsuffix  = "-#{net_uid}"
-    cnamesuffix = nil # no CNAME entry by default
+    new_record = DNS::Zone::RR::A.new
+    new_record.address = net_hash['ip']
+    new_record.label = "#{cluster_uid}-#{node_id}"
+    new_record.label += "-#{net_uid}" unless net_hash['mounted'] && /^eth[0-9]$/.match(net_uid)
+    records << new_record
 
     if net_hash['mounted'] && /^eth[0-9]$/.match(net_uid)
-      #primary interface
-      cnamesuffix = '' # CNAME enabled for primary interface (node-id-iface cname node-id)
-    elsif /^*-kavlan-[0-9]*$/.match(net_uid)
-      # kavlan
-      net_primaries = network_adapters.select{ |u, h| h['mounted'] && /^eth[0-9]$/.match(u) } # list of primary interfaces
-      net_uid_eth, net_uid_kavlan = net_uid.to_s.scan(/^([^-]*)-(.*)$/).first # split 'eth0-kavlan-1'
-      
-      # CNAME only for primary interface kavlan
-      if net_primaries.include?(net_uid_eth)
-        hostsuffix  = "-#{net_uid_kavlan}" # -kavlan-1
-        cnamesuffix = "-#{net_uid}"        # -eth0-kavlan-1
-      end
+      # CNAME enabled for primary interface (node-id-iface cname node-id)
+      cname_record = DNS::Zone::RR::CNAME.new
+      cname_record.label = "#{cluster_uid}-#{node_id}-#{net_uid}"
+      cname_record.domainname = "#{cluster_uid}-#{node_id}"
+      records << cname_record
     end
-    new_entry = {
-      :uid         => cluster_uid,
-      :hostsuffix  => hostsuffix, # -eth0, -kavlan-1
-      :cnamesuffix => cnamesuffix,   # graoully-$-, graoully-$-eth0-kavlan-1
-      :node_uid    => node_id,
-      :ip          => ip
-    }
+
     #Handle interface aliases
     if (net_hash["alias"])
-      new_entry[:cnames] ||= []
       net_hash["alias"].each { |cname|
-        new_entry[:cnames] << "#{cluster_uid}-#{node_id}-#{cname}"
+        cname_record = DNS::Zone::RR::CNAME.new
+        cname_record.label = "#{cluster_uid}-#{node_id}-#{cname}"
+        cname_record.domainname = "#{cluster_uid}-#{node_id}"
+        records << cname_record
       }
     end
-    #Group entries by cluster and cluster-kavlan
-    if (/kavlan/.match(net_uid))
-      entries["#{cluster_uid}-kavlan"] ||= []
-      entries["#{cluster_uid}-kavlan"] << new_entry
-    else
-      entries["#{cluster_uid}"] ||= []
-      entries["#{cluster_uid}"] << new_entry
+  } #each network adapters
+  return records
+end
+
+def get_node_kavlan_records(cluster_uid, node_uid, network_adapters, kavlan_adapters)
+  records = []
+
+  kavlan_adapters.each { |net_uid, net_hash|
+
+    next unless net_hash['ip']
+
+    net_primaries = network_adapters.select{ |u, h| h['mounted'] && /^eth[0-9]$/.match(u) } # list of primary interfaces
+    net_uid_eth, net_uid_kavlan = net_uid.to_s.scan(/^([^-]*)-(.*)$/).first # split 'eth0-kavlan-1'
+
+    new_record = DNS::Zone::RR::A.new
+    new_record.address = net_hash['ip']
+    new_record.label = "#{node_uid}-#{net_uid}" #sol-23-eth0-kavlan-1
+    records << new_record
+
+    # CNAME only for primary interface kavlan
+    if net_primaries.include?(net_uid_eth)
+      cname_record = DNS::Zone::RR::CNAME.new
+      cname_record.label = "#{node_uid}-#{net_uid_kavlan}"
+      cname_record.domainname = "#{node_uid}-#{net_uid}" #sol-23-eth0-kavlan-1
+      records << cname_record
     end
   } #each network adapters
-  return entries
+
+  return records
 end
 
-puts "Writing DNS configuration files to: #{options[:output_dir]}"
-puts "For site(s): #{options[:sites].join(', ')}"
+def get_reverse_record(record, site_uid)
+
+  return unless record.is_a?(DNS::Zone::RR::A)
+
+  ip_array = record.address.split(".")
+  file_name = "reverse-#{ip_array[0..2].reverse.join('.')}.db" # 70.16.172
+
+  if /.*-kavlan-[1-3]$/.match(record.label)
+    #A filter in bind-global-site.conf.erb prevents entries in 'local' directory to be included in global configuration
+    #TODO later, also add DMZ IPs check here
+    file_name.prepend("local/")
+  end
+
+  reverse_record = DNS::Zone::RR::PTR.new
+  reverse_record.label = ip_array[3] #ip suffix
+  reverse_record.name = "#{record.label}.#{site_uid}.grid5000.fr."
+
+  return file_name, reverse_record
+end
+
+def sort_records(records)
+  sorted_records = []
+  cnames = []
+  in_a = []
+  ptr = []
+
+  records.each{ |record|
+    if (record.is_a?(DNS::Zone::RR::A))
+      in_a << record
+    elsif (record.is_a?(DNS::Zone::RR::CNAME))
+      cnames << record
+    elsif (record.is_a?(DNS::Zone::RR::PTR))
+      ptr << record
+    end
+  }
+  in_a.sort_by!{ |record|
+    record.address.split('.').map{ |octet|
+      octet.to_i
+    }
+  }
+  sorted_records += in_a
+  ptr.sort_by!{ |record|
+    record.name
+  }
+  sorted_records += ptr
+  #Sort CNAMES by node_id for node, node_id then kavlan number for node kavlan entry or finally by label
+  cnames.sort_by!{ |record|
+    sort_by = record.label
+    label_array = record.label.split("-")
+    if label_array.length == 4
+      if label_array[1].to_i != 0 && label_array[3].to_i != 0
+        sort_by = [label_array[3].to_i, label_array[1].to_i]
+      end
+    elsif label_array.length > 1
+      if label_array[1].to_i != 0
+        sort_by = label_array[1].to_i
+      end
+    end
+    sort_by
+  }
+  sorted_records += cnames
+  return sorted_records
+end
+
+def include_manual_file(zone)
+  manual_file_path = File.join(File.dirname(zone.file_path), File.basename(zone.file_path).sub('.db', '') + '-manual.db')
+  if (File.exists?(manual_file_path))
+    return "$INCLUDE /etc/bind/zones/#{zone.site_uid}/#{File.basename(zone.file_path).sub('.db', '') + '-manual.db'}\n"
+  end
+  return ''
+end
+
+#
+def load_zone(zone_file_path, site_uid, site, header)
+  if File.exists?(zone_file_path)
+    zone = DNS::Zone.load(File.read(zone_file_path))
+  else
+    zone = DNS::Zone.new
+  end
+  zone.site_uid = site_uid
+  zone.file_path = zone_file_path
+  #If the target file will have a header or not (manage included files without records duplication)
+  zone.header = header
+  zone.soa = zone.records[0] if zone.records.any? && zone.records[0].type == 'SOA'
+
+  #We only want the zone to manage these records, we will manage SOA, MX, NS, @s manually
+  zone.records.reject! { |rec|
+    !( rec.is_a?(DNS::Zone::RR::A) || rec.is_a?(DNS::Zone::RR::CNAME) || rec.is_a?(DNS::Zone::RR::PTR)) || rec.label == "@"
+  }
+  if header
+    set_zone_header_records(zone, site)
+  end
+  zone.include = include_manual_file(zone)
+  return zone
+end
+
+def set_zone_header_records(zone, site)
+  if zone.soa.nil?
+    soa = DNS::Zone::RR::SOA.new
+    soa.serial = Time.now.utc.strftime("%Y%m%d00")
+    soa.refresh_ttl = "4h"
+    soa.retry_ttl = "1h"
+    soa.expiry_ttl = "4w"
+    soa.minimum_ttl = "1h"
+    soa.nameserver = "dns.grid5000.fr."
+    soa.email = "nsmaster.dns.grid5000.fr."
+    zone.soa = soa
+  end
+  zone.ns = DNS::Zone::RR::NS.new
+  zone.ns.nameserver = "dns.grid5000.fr."
+  zone.mx = DNS::Zone::RR::MX.new
+  zone.mx.priority = 10
+  zone.mx.exchange = "mail.#{zone.site_uid}.grid5000.fr."
+  if (File.basename(zone.file_path) == "#{zone.site_uid}.db")
+    zone.at = DNS::Zone::RR::A.new
+    zone.at.address = site['frontend_ip']
+  end
+end
+
+def update_serial(serial)
+  date_serial = DateTime.strptime(serial.to_s, "%Y%m%d%S")
+  now_date = DateTime.now
+  if (date_serial.strftime("%Y%m%d") == now_date.strftime("%Y%m%d"))
+    date_serial = date_serial + Rational(1, 86400) #+1 second
+    return date_serial.strftime("%Y%m%d%S")
+  else
+    return Time.now.utc.strftime("%Y%m%d00")
+  end
+end
+
+#Check if there are differences between existing and newly created records
+#We check only A and CNAME records
+def diff_zone_file(zone, records)
+  #Compare dumped strings directly instead of RR objects
+  zone_records = zone.records.map{ |rec|
+    rec.dump
+  }
+  recs = records.map{ |rec|
+    rec.dump
+  }
+  removed_records = zone_records - recs
+  added_records = recs - zone_records
+  if $options[:verbose]
+    if removed_records.any?
+      puts "Removed records in zone file: #{zone.file_path}"
+      removed_records.each{ |rec|
+        puts rec
+      }
+    end
+    if added_records.any?
+      puts "Added records in zone file: #{zone.file_path}"
+      added_records.each{ |rec|
+        puts rec
+      }
+    end
+  end
+  return added_records.any? || removed_records.any?
+end
+
+#Create reverse-*.db files for each reverse-*-manual.db that do not have (yet) a corresponding file.
+def get_orphan_reverse_manual_zones(zones_dir, site_uid, site)
+  zones = []
+  Dir.glob(File.join(zones_dir, "reverse-*-manual.db")).each { |reverse_manual_file|
+    output_file = reverse_manual_file.sub("-manual.db", ".db")
+    next if File.exists?(File.join(output_file))
+    puts "Creating file for orphan reverse manual file: #{output_file}" if $options[:verbose]
+    #Creating the zone will include automatically the manual file
+    zone = load_zone(output_file, site_uid, site, true)
+    zones << zone
+  }
+  return zones
+end
+
+def write_global_site_conf(site_uid, dest_dir, zones_dir)
+  ['global', 'site'].each { |dir|
+    conf_file = File.join(dest_dir, "#{dir}/conf", "global-#{site_uid}.conf")
+    FileUtils.mkdir_p(File.dirname(conf_file))
+    conf_content = ERB.new(File.read(File.expand_path('templates/bind-global-site.conf.erb', File.dirname(__FILE__)))).result(binding)
+    File.write(conf_file, conf_content)
+  }
+end
+
+def write_zone(zone)
+  FileUtils.mkdir_p(File.dirname(zone.file_path))
+  File.write(zone.file_path, zone.dump)
+end
+
+puts "Writing DNS configuration files to: #{$options[:output_dir]}"
+puts "For site(s): #{$options[:sites].join(', ')}"
+
+puts "Note: if you modify *-manual.db files you will have to manually update the serial in managed db file for changes to be applied"
 
 $written_files = []
 
 # Loop over Grid'5000 sites
 refapi["sites"].each { |site_uid, site|
+  
+  next unless $options[:sites].include?(site_uid)
 
-  next unless options[:sites].include?(site_uid)
+  dest_dir = "#{$options[:output_dir]}/modules/bindg5k/files/"
+  zones_dir = File.join(dest_dir, "zones/#{site_uid}")
 
-  entries = {}
+  site_records = {}
 
   # Servers
-  entries['servers'] = get_servers_entries(site) unless site['servers'].nil?
+  site_records['servers'] = get_servers_records(site) unless site['servers'].nil?
 
   # PDUs
-  entries['pdus'] = get_pdus_entries(site) unless site['pdus'].nil?
+  site_records['pdus'] = get_pdus_records(site) unless site['pdus'].nil?
 
   # Networks and laptops (same input format)
-  entries['networks'] = get_networks_entries(site, 'networks') unless site['networks'].nil?
-  entries['laptops'] = get_networks_entries(site, 'laptops') unless site['laptops'].nil?
+  site_records['networks'] = get_networks_records(site, 'networks') unless site['networks'].nil?
+  site_records['laptops'] = get_networks_records(site, 'laptops') unless site['laptops'].nil?
 
   site.fetch("clusters").sort.each { |cluster_uid, cluster|
 
@@ -323,88 +501,93 @@ refapi["sites"].each { |site_uid, site|
         network_adapters[net_uid] = {"ip" => net_hash["ip"], "mounted" => net_hash["mounted"], "alias" => net_hash["alias"]}
       }
 
+      # Mic
+      if node['mic'] && node['mic']['ip']
+        network_adapters['mic0'] = {"ip" => node['mic']['ip']}
+      end
+
+      site_records[cluster_uid] ||= []
+      site_records[cluster_uid] += get_node_records(cluster_uid, node_uid, network_adapters)
+
       # Kavlan
       if node['kavlan']
+        kavlan_adapters = {}
         node.fetch('kavlan').each { |net_uid, net_hash|
           net_hash.each { |kavlan_net_uid, ip|
-            network_adapters["#{net_uid}-#{kavlan_net_uid}"] = {"ip" => ip, "mounted" => nil}
+            kavlan_adapters["#{net_uid}-#{kavlan_net_uid}"] = {"ip" => ip, "mounted" => node['network_adapters'][net_uid]['mounted']}
           }
         }
+        site_records["#{cluster_uid}-kavlan"] ||= []
+        site_records["#{cluster_uid}-kavlan"] += get_node_kavlan_records(cluster_uid, node_uid, network_adapters, kavlan_adapters)
       end
-
-      # Mic
-      if node['mic'] && node['mic']['ip']
-        network_adapters['mic0'] = {"ip" => node['mic']['ip']}
-      end
-
-      node_entries = get_node_entries(cluster_uid, node_uid, network_adapters)
-      node_entries.each { |k, n_entries|
-        entries[k] ||= []
-        entries[k] += n_entries
-      }
     } # each nodes
   } # each cluster
 
-  #
-  # Output
-  #
-  dns = {}     # one hash entry per type output file (e.g: nancy/servers.db)
-  reverse = {} # one hash entry per reverse dns file
-  local_reverse_list = [] # reverse zone files that include local kavlan (kavlan-1,kavlan-2,kavlan-3).
+  reverse_records = {} # one hash entry per reverse dns file
 
-  entries.each { |type, e|
+  site_records.each { |zone, records|
 
-    dns[type] ||= {}
-    dns[type]["addr"] ||= []
-    dns[type]["cnames"] ||= []
+    #Sort records
+    site_records[zone] = sort_records(records)
 
-    e.sort_by! { |entry|
-      entry[:ip].split('.').map{ |octet|
-        octet.to_i
-      }
+    records.each{ |record|
+      #get Reverse records
+      reverse_file_name, reverse_record = get_reverse_record(record, site_uid)
+      if reverse_file_name != nil
+        reverse_records[reverse_file_name] ||= []
+        reverse_records[reverse_file_name] << reverse_record
+      end
     }
+  }
 
-    e.each { |entry|
+  zones = []
 
-      addr_entries, cname_entries = print_entry(entry)
-      dns[type]["addr"] += addr_entries
-      if (! cname_entries.empty?)
-        dns[type]["cnames"] += cname_entries
-      end
+  #Sort reverse records and create reverse zone from files
+  reverse_records.each{ |file_name, records|
+    records.sort!{ |a, b|
+      a.label.to_i <=> b.label.to_i
+    }
 
-      #Reverse entries
-      reverse_output_file, txt_entry = print_reverse_entry(site_uid, entry) # Reverse DNS
-      if /.*-kavlan-[1-3]$/.match(entry[:hostsuffix])
-        reverse_output_file.prepend("local/")
-        local_reverse_list << reverse_output_file
-      end
+    reverse_file_path = File.join(zones_dir, file_name)
+    zone = load_zone(reverse_file_path, site_uid, site, true)
+    if diff_zone_file(zone, records)
+      zone.soa.serial = update_serial(zone.soa.serial)
+    end
+    zone.records = records;
+    zones << zone
+  }
 
-      reverse[reverse_output_file] ||= []
-      reverse[reverse_output_file] << txt_entry
-    }
+  #Manage site zone (SITE.db file)
+  #It only contains header and inclusion of other db files
+  #Check modification in included files and update serial accordingly
+  site_zone_path = File.join(zones_dir, site_uid + ".db")
+  site_zone = load_zone(site_zone_path, site_uid, site, true)
+  site_zone_changed = false
+
+  site_records.each{ |type, records|
+    zone_file_path = File.join(zones_dir, site_uid + "-" + type + ".db")
+    zone = load_zone(zone_file_path, site_uid, site, false)
+    if diff_zone_file(zone, records)
+      puts "Zone file changed: #{zone.file_path}" if $options[:verbose]
+      site_zone_changed = true
+    end
+    zone.records = records
+    site_zone.include += "$INCLUDE /etc/bind/zones/#{site_uid}/#{File.basename(zone_file_path)}\n"
+    zones << zone
   }
 
-  dest_dir = "#{options[:output_dir]}/modules/bindg5k/files/"
-  zones_dir = File.join(dest_dir, "zones/#{site_uid}")
+  if (site_zone_changed)
+    site_zone.soa.serial = update_serial(site_zone.soa.serial)
+  end
+
+  zones << site_zone
 
-  write_site_zones(site_uid, site, zones_dir, dns)
+  zones += get_orphan_reverse_manual_zones(zones_dir, site_uid, site)
 
-  write_reverse_site_zones(site_uid, zones_dir, reverse)
+  zones.each{ |zone|
+    write_zone(zone)
+  }
 
   write_global_site_conf(site_uid, dest_dir, zones_dir)
 
 } # each sites
-
-# Revert changes on files where only the serial has been updated
-$written_files.each { |filePath|
-
-  Dir.chdir("#{options[:output_dir]}") {
-    if (Dir.exists?("./.git"))
-      added_deleted = `git diff --numstat #{filePath} | cut -f1-2 | tr -d "\t "`.chomp()
-      if (added_deleted == "11") #1 addition, 1 deletion
-        system("git checkout -- #{filePath}")
-      end
-    end
-  }
-
-}
diff --git a/generators/puppet/templates/bind-header.erb b/generators/puppet/templates/bind-header.erb
deleted file mode 100644
index 948bf0fd17cdcbce8503c226444f263161833ad5..0000000000000000000000000000000000000000
--- a/generators/puppet/templates/bind-header.erb
+++ /dev/null
@@ -1,12 +0,0 @@
-$TTL 3h
-@                       IN      SOA     dns.grid5000.fr. nsmaster.dns.grid5000.fr. (
-                                <%= Time.new().strftime("%Y%m%d") %>00 ; serial (YYYYMMDDSS)
-                                4h         ; refresh
-                                1h         ; retry
-                                4w         ; expire
-                                1h)        ; negative caching
-
-@			IN	NS	dns.grid5000.fr.<%
-if output_file == "#{site_uid}.db" %>
-@                      IN      A       <%= site['frontend_ip'] %><% end %>
-@                 MX    10      mail.<%= site_uid %>.grid5000.fr.