Project

General

Profile

« Previous | Next » 

Revision 66a02240

Added by Marc Dequènes almost 14 years ago

  • ID 66a0224083dc5772259332902df4aab946afa4bd

[evol] DNS API rework §1

View differences:

bin/mapmaker
$: << File.join(File.dirname(__FILE__), "..", "lib")
require 'cyborghood/cyborg'
require 'cyborghood/services/dns'
require 'cyborghood-mapmaker/dns'
module CyborgHood
bin/test_client
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
lib/cyborghood-mapmaker/dns.rb
#--
# 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
lib/cyborghood-mapmaker/interface/0_base.rb
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
lib/cyborghood-mapmaker/interface/_zone/0_base.rb
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?
lib/cyborghood-mapmaker/interface/dns.rb
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
lib/cyborghood-mapmaker/interface/services/dns.rb
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
lib/cyborghood/command_runner.rb
require 'shellwords'
require 'cyborghood/order'
require 'cyborghood/objects'
require 'cyborghood/services/dns'
module CyborgHood
lib/cyborghood/services/dns.rb
#--
# 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

Also available in: Unified diff