|
#--
|
|
# CyborgHood, a distributed system management software.
|
|
# Copyright (c) 2009-2011 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) if has_signed_zone_file?
|
|
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)
|
|
begin
|
|
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
|
|
rescue Dnsruby::ZoneReader::ParseException
|
|
# return nil is not parseable
|
|
end
|
|
end
|
|
|
|
def parsed_signed_content
|
|
return unless has_signed_zone_file?
|
|
|
|
begin
|
|
parsed_content = ZoneContent.new(@name)
|
|
parsed_content.import_from_file(@filename_signed)
|
|
parsed_content
|
|
rescue Dnsruby::ZoneReader::ParseException
|
|
# return nil is not parseable
|
|
end
|
|
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
|
|
hszf = self.has_signed_zone_file?
|
|
p_c = self.parsed_content
|
|
data = {
|
|
:is_master => self.master?,
|
|
:has_signed_zone_file => hszf,
|
|
:serial_in_zone_file => p_c.nil? ? nil : p_c.serial
|
|
}
|
|
|
|
if hszf
|
|
s_p_c = self.parsed_signed_content
|
|
data.merge!({
|
|
:serial_in_signed_zone_file => s_p_c.nil? ? nil : s_p_c.serial
|
|
})
|
|
end
|
|
|
|
data
|
|
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
|