|
#--
|
|
# 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'
|
|
|
|
# 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
|
|
|
|
@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
|
|
Dir.glob(@zone_files_pattern).collect do |file|
|
|
$1 if file =~ @zone_files_regex
|
|
end
|
|
end
|
|
|
|
def [](zone)
|
|
return unless zones.include?(zone)
|
|
|
|
Zone.new(zone)
|
|
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(zone)
|
|
@zone = zone
|
|
|
|
@config = Config.instance
|
|
@content = nil
|
|
@temp_file = nil
|
|
|
|
@filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
|
|
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 serial(force_real = false)
|
|
r = check_zone_file('none', force_real = false)
|
|
if r.ok
|
|
r.serial
|
|
else
|
|
raise CyberError.new(:unrecoverable, "services/dns", "zone serial for '#{@zone}' could not be found (#{r.errors.first})")
|
|
end
|
|
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} '#{@zone}' >/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(@zone)
|
|
@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 '#{@zone}' 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 '#{@zone}' 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} '#{@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
|
|
|
|
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
|