Revision 90197e7b
Added by Marc Dequènes over 13 years ago
- ID 90197e7bd70d31f0ef7cd6dde8f2e95b6dc08ab5
bin/mapmaker | ||
---|---|---|
# to allow in-place run for test
|
||
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
||
|
||
require 'needle'
|
||
require 'cyborghood/cyborg'
|
||
require 'cyborghood-mapmaker/dns'
|
||
|
||
|
||
module CyborgHood
|
||
... | ... | |
end
|
||
end
|
||
|
||
module MapMakerHome
|
||
module MapMakerLand
|
||
include I18nTranslation
|
||
bindtextdomain("cyborghood_mapmaker", {:path => Config::L10N_DIR, :charset => "UTF-8"})
|
||
|
||
... | ... | |
end
|
||
end
|
||
|
||
bot = CyborgHood::MapMakerHome::MapMaker.new
|
||
reg = Needle::Registry.new
|
||
reg.define do |b|
|
||
b.require 'cyborghood', CyborgHood
|
||
b.require 'cyborghood-mapmaker/land', CyborgHood::MapMakerLand
|
||
|
||
b.bot { CyborgHood::MapMakerLand::MapMaker.new(b.mapmaker_land) }
|
||
end
|
||
|
||
bot = reg.bot
|
||
|
||
trap('INT') do
|
||
bot.ask_to_stop
|
bin/test_client | ||
---|---|---|
ask "Librarian", :info2, "/_cyborg_"
|
||
ask "MapMaker", :zones, "/Zones"
|
||
#ask "MapMaker", :wanted_failure, "/prout"
|
||
ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
|
||
ask "MapMaker", :dns, "/Services/DNS"
|
||
ask "MapMaker", :dnssec, "/Services/DNSSEC"
|
||
ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
|
||
ask "MapMaker", :dns_check, "/Services/DNS/check_config"
|
||
#ask "MapMaker", :search, "/Zones/?"
|
||
#ask "MapMaker", :search_master, "/Zones/?", {:master => true}
|
data/cyborghood/default_config/mapmaker.yaml | ||
---|---|---|
---
|
||
dns:
|
||
software: bind
|
||
software: bind
|
||
dnssec:
|
||
software: opendnssec
|
data/cyborghood/schema/mapmaker.yaml | ||
---|---|---|
"signed_master_zone_pattern": {type: str, name: MasterZonePattern}
|
||
"slave_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
|
||
"update_zone_script": {type: str}
|
||
"dnssec":
|
||
type: map
|
||
mapping:
|
||
"software": {type: str, enum: [opendnssec]}
|
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/dns/base.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 '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) unless @filename_signed.nil?
|
||
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)
|
||
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
|
||
end
|
||
|
||
def parsed_signed_content
|
||
return if @filename_signed.nil?
|
||
|
||
parsed_content = ZoneContent.new(@name)
|
||
parsed_content.import_from_file(@filename_signed)
|
||
parsed_content
|
||
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
|
||
{
|
||
:is_master => self.master?,
|
||
:has_signed_zone_file => self.has_signed_zone_file?,
|
||
:serial_in_zone_file => self.parsed_content.serial,
|
||
:serial_in_signed_zone_file => self.parsed_signed_content.serial
|
||
}
|
||
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
|
lib/cyborghood-mapmaker/dns/bind.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 'cyborghood-mapmaker/dns/base'
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class DNS < DNSBase
|
||
def check_config
|
||
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
|
||
|
||
def info
|
||
super.merge(self.status)
|
||
end
|
||
|
||
def status
|
||
output = []
|
||
begin
|
||
IO.popen("sudo rndc status") 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 DNSZoneFile < DNSZoneFileBase
|
||
def activate
|
||
system "sudo rndc reload '#{@name}' >/dev/null"
|
||
raise CyberError.new(:unrecoverable, "services/dns", "zone activation failed") unless $?.success?
|
||
end
|
||
|
||
def check(on_disk = false)
|
||
check_zone_file('full', on_disk = false)
|
||
end
|
||
|
||
protected
|
||
|
||
def check_zone_file(check_type, on_disk = false)
|
||
filename = on_disk ? @filename : current_filename
|
||
|
||
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 # MapMakerLand
|
||
end
|
lib/cyborghood-mapmaker/dnssec/base.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/>.
|
||
#++
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class DNSSECBase
|
||
def initialize(config)
|
||
@config = config
|
||
|
||
flush_cache
|
||
end
|
||
|
||
def zones
|
||
@zone_list.keys
|
||
end
|
||
|
||
def get_zone(zone_name)
|
||
@zone_list[zone_name]
|
||
end
|
||
|
||
def software
|
||
@config.dnssec.software
|
||
end
|
||
|
||
def info
|
||
{
|
||
:software => self.software
|
||
}
|
||
end
|
||
|
||
def flush_cache
|
||
fetch_zone_list()
|
||
end
|
||
end
|
||
|
||
class DNSSECZoneBase
|
||
attr_reader :name, :input_file, :output_file, :params
|
||
|
||
def initialize(config, name, input_file, output_file, params)
|
||
@config = config
|
||
@name = name
|
||
@input_file = input_file
|
||
@output_file = output_file
|
||
# backend-specific parameters, used for checks or generating useful info
|
||
@params = params
|
||
end
|
||
|
||
def info
|
||
{
|
||
:dnssec_input_file => @input_file,
|
||
:dnssec_output_file => @output_file
|
||
}
|
||
end
|
||
|
||
# methods a backend MUST implement
|
||
# - resign
|
||
end
|
||
end # MapMakerLand
|
||
end
|
lib/cyborghood-mapmaker/dnssec/opendnssec.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 'cyborghood-mapmaker/dnssec/base'
|
||
require "rexml/document"
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class DNSSEC < DNSSECBase
|
||
|
||
protected
|
||
|
||
def fetch_zone_list
|
||
output = []
|
||
begin
|
||
IO.popen("sudo ods-control ksm zonelist export 2>/dev/null") do |fp|
|
||
output << fp.gets.chomp! until fp.eof?
|
||
end
|
||
rescue
|
||
raise CyberError.new(:unrecoverable, "services/dnssec", "dnssec zone list could not be checked (I/O error)")
|
||
end
|
||
|
||
@zone_list = {}
|
||
doc = REXML::Document.new(output.join("\n"))
|
||
doc.elements.each('ZoneList/Zone') do |e|
|
||
zone_name = e.attributes['name']
|
||
@zone_list[zone_name] =
|
||
DNSSECZone.new(@config, zone_name,
|
||
e.elements['Adapters/Input/File'].text,
|
||
e.elements['Adapters/Output/File'].text,
|
||
{ :policy => e.elements['Policy'] })
|
||
end
|
||
end
|
||
end
|
||
|
||
class DNSSECZone < DNSSECZoneBase
|
||
def resign
|
||
system "sudo ods-control signer sign '#{@name}' >/dev/null"
|
||
raise CyberError.new(:unrecoverable, "services/dnssec", "zone resign failed") unless $?.success?
|
||
end
|
||
end
|
||
end # MapMakerLand
|
||
end
|
lib/cyborghood-mapmaker/interface/0_base.rb | ||
---|---|---|
store.dns = Services::DNS::System.new
|
||
store.dns = bot.services.dns
|
||
|
||
node 'Services', :dir => 'services'
|
||
|
||
node 'Zones' do
|
||
zone_list = Proc.new{ store.dns.zones }
|
||
zone_list = Proc.new do
|
||
store.dns.flush_cache
|
||
store.dns.zones
|
||
end
|
||
|
||
attr_search_node
|
||
|
lib/cyborghood-mapmaker/interface/_zone/0_base.rb | ||
---|---|---|
zone = Services::DNS::Zone.new(node_name)
|
||
bot.services.dnssec.flush_cache
|
||
zone = bot.services.zone(node_name)
|
||
|
||
on_request do |request|
|
||
request.reply.results = {
|
||
:master => zone.master?,
|
||
:signed => zone.signed?,
|
||
:serial => zone.serial_in_dns
|
||
}
|
||
if zone.master?
|
||
request.reply.results.merge!({
|
||
:serial_in_zone_file => zone.serial_in_zone_file,
|
||
:serial_in_signed_zone_file => zone.serial_in_signed_zone_file
|
||
})
|
||
end
|
||
request.reply.results = zone.info
|
||
end
|
||
|
||
node 'content' do
|
||
... | ... | |
return
|
||
end
|
||
|
||
zone.content = content
|
||
if zone.changed?
|
||
check_result = zone.check
|
||
if check_result[:ok]
|
||
request.reply.warnings = check_result[:warnings]
|
||
# zone signer automatically handles serial bump
|
||
if check_result[:serial] > zone.serial or zone.signed?
|
||
zone.save
|
||
zone.activate
|
||
else
|
||
request.reply.errors << _("Zone serial is not superior to current serial.")
|
||
end
|
||
else
|
||
request.reply.errors = check_result[:errors]
|
||
zone.cancel_changes
|
||
end
|
||
else
|
||
request.reply.warnings << _("Zone is unmodified (same content)")
|
||
zone.cancel_changes
|
||
begin
|
||
zone.content = content
|
||
rescue CyberError => e
|
||
request.reply.errors << e.message
|
||
end
|
||
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)
|
||
request.reply.results = store.dns.info
|
||
end
|
||
|
||
node 'check_config' do
|
lib/cyborghood-mapmaker/interface/services/dnssec.rb | ||
---|---|---|
node 'DNSSEC' do
|
||
on_request do |request|
|
||
request.reply.results = bot.services.dnssec.info
|
||
end
|
||
end
|
lib/cyborghood-mapmaker/land.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/>.
|
||
#++
|
||
|
||
# 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 MapMakerLand
|
||
def register_services(container)
|
||
container.namespace_define(:mapmaker_land) do |b|
|
||
b.dns do
|
||
require 'cyborghood-mapmaker/dns/' + b.config.dns.software
|
||
DNS.new(b.config)
|
||
end
|
||
|
||
b.dnssec do
|
||
require 'cyborghood-mapmaker/dnssec/' + b.config.dnssec.software
|
||
DNSSEC.new(b.config)
|
||
end
|
||
|
||
b.zone_editor do
|
||
require 'cyborghood-mapmaker/zone_editor'
|
||
ZoneEditor.new()
|
||
end
|
||
|
||
b.zone :model => :multiton do |c, p, name|
|
||
require 'cyborghood-mapmaker/zone'
|
||
Zone.new(c.config, c.dns, c.dnssec, c.zone_editor, name)
|
||
end
|
||
end
|
||
end
|
||
|
||
module_function :register_services
|
||
end
|
||
end
|
lib/cyborghood-mapmaker/zone.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/>.
|
||
#++
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class Zone
|
||
def initialize(config, dns, dnssec, zone_editor, name)
|
||
@config = config
|
||
@dns = dns
|
||
@dnssec = dnssec
|
||
@zone_editor = zone_editor
|
||
@name = name
|
||
|
||
@dns_zone = @dns.get_zone(@name)
|
||
@dns_zone_file = @dns.get_zone_file(@name)
|
||
@dnssec_zone = @dnssec.get_zone(@name)
|
||
end
|
||
|
||
def exists?
|
||
not @dns_zone.nil? and not @dns_zone_file.nil?
|
||
end
|
||
|
||
def info
|
||
i = {}
|
||
i.merge!(@dns_zone.info) unless @dns_zone.nil?
|
||
i.merge!(@dns_zone_file.info) unless @dns_zone_file.nil?
|
||
i.merge!(@dnssec_zone.info) unless @dnssec_zone.nil?
|
||
i
|
||
end
|
||
|
||
def content
|
||
@dns_zone_file.content
|
||
end
|
||
|
||
def content=(c)
|
||
@dns_zone_file.content = c
|
||
|
||
unless @dns_zone_file.changed?
|
||
raise CyberError.new(:unrecoverable, "zone", "zone did not change")
|
||
end
|
||
|
||
report = @dns_zone_file.check
|
||
unless report[:ok]
|
||
raise CyberError.new(:unrecoverable, "zone", "zone content is buggy: " + report[:errors].join(", "))
|
||
end
|
||
|
||
zone_signed = @dns_zone.signed?
|
||
|
||
# DNSSEC tools automatically increase serial
|
||
unless zone_signed
|
||
unless @dns_zone_file.parsed_content.serial > @dns_zone.serial
|
||
raise CyberError.new(:unrecoverable, "zone", "zone content serial is not superior to current serial")
|
||
end
|
||
end
|
||
|
||
@dns_zone_file.create_backup
|
||
@dns_zone_file.save
|
||
begin
|
||
if zone_signed
|
||
@dnssec_zone.resign
|
||
else
|
||
@dns_zone_file.activate
|
||
end
|
||
rescue
|
||
@dns_zone_file.import_from_backup
|
||
@dns_zone_file.save
|
||
raise CyberError.new(:unrecoverable, "zone", "zone activation failed, replacing old content")
|
||
end
|
||
end
|
||
|
||
def alter(recipe)
|
||
# TODO: use a ZoneEditor to handle the recipe and save the result
|
||
# TODO: put as many things in common with content=()
|
||
# TODO: auto-increase the serial if not already done in the recipe and the zone is not signed
|
||
end
|
||
|
||
def check
|
||
report = {
|
||
:errors => [],
|
||
:warnings => []
|
||
}
|
||
|
||
if @dns_zone.signed?
|
||
if @dns_zone_file.filename != @dnssec_zone.input_file
|
||
report[:errors] << _("DNS and DNSSEC original zone files do not match")
|
||
end
|
||
if @dns_zone_file.filename_signed != @dnssec_zone.output_file
|
||
report[:errors] << _("DNS and DNSSEC signed zone files do not match")
|
||
end
|
||
end
|
||
|
||
zone_file_serial = @dns_zone.signed? ? @dns_zone_file.parsed_signed_content.serial :
|
||
@dns_zone_file.parsed_content.serial
|
||
if zone_file_serial != @dns_zone.serial
|
||
report[:warnings] << _("The zone serial does not match the one in the zone file")
|
||
end
|
||
|
||
# TODO: more checks
|
||
|
||
report
|
||
end
|
||
end
|
||
end # MapMakerLand
|
||
end
|
lib/cyborghood-mapmaker/zone_content.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 'dnsruby'
|
||
require 'tempfile'
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class ZoneContentBase
|
||
def initialize(name)
|
||
@name = name
|
||
end
|
||
|
||
def soa
|
||
find_rr('SOA').first
|
||
end
|
||
|
||
def serial
|
||
s = self.soa
|
||
s ? s.serial : nil
|
||
end
|
||
|
||
def signed?
|
||
not find_rr('DNSKEY').empty?
|
||
end
|
||
|
||
# methods a backend MUST implement
|
||
# - find_rr
|
||
end
|
||
|
||
class ZoneContent < ZoneContentBase
|
||
def initialize(name)
|
||
super
|
||
|
||
@reader = Dnsruby::ZoneReader.new(@name)
|
||
@zone = []
|
||
end
|
||
|
||
def content
|
||
@zone.collect{|rr| rr.to_s }.join("\n")
|
||
end
|
||
|
||
def content=(str)
|
||
begin
|
||
temp_file = Tempfile.new(@name)
|
||
temp_file.write(str)
|
||
temp_file.close
|
||
rescue
|
||
raise CyberError.new(:unrecoverable, "services/dns", "could not save temporary zone")
|
||
end
|
||
|
||
import_from_file(temp_file.path)
|
||
|
||
temp_file.close!
|
||
end
|
||
|
||
def import_from_file(file)
|
||
@zone = @reader.process_file(file) || []
|
||
end
|
||
|
||
def to_s
|
||
self.content
|
||
end
|
||
|
||
def empty?
|
||
@zone.empty?
|
||
end
|
||
|
||
def find_rr(rr_type)
|
||
@zone.select{|rr| rr.name.to_s == @name and rr.type == rr_type }
|
||
end
|
||
|
||
# TODO: methods to add/replace RRs
|
||
end
|
||
end
|
||
end
|
lib/cyborghood-mapmaker/zone_editor.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 'cyborghood-mapmaker/zone_content'
|
||
|
||
|
||
module CyborgHood
|
||
module MapMakerLand
|
||
class ZoneEditor
|
||
end
|
||
end # MapMakerLand
|
||
end
|
lib/cyborghood.rb | ||
---|---|---|
$KCODE = 'UTF8'
|
||
require 'jcode'
|
||
require 'pp'
|
||
require 'needle'
|
||
require 'cyborghood/base/config'
|
||
require 'cyborghood/base/info'
|
||
require 'cyborghood/base/lang_additions'
|
||
... | ... | |
require 'cyborghood/base/language'
|
||
require 'cyborghood/base/exceptions'
|
||
|
||
|
||
module CyborgHood
|
||
include I18nTranslation
|
||
ENV['LC_ALL'] = "C"
|
||
bindtextdomain("cyborghood", {:path => Config::L10N_DIR, :charset => "UTF-8"})
|
||
|
||
def register_services(container)
|
||
container.define do |b|
|
||
b.config do
|
||
Config.instance
|
||
end
|
||
end
|
||
end
|
||
|
||
module_function :register_services
|
||
end
|
lib/cyborghood/cyborg.rb | ||
---|---|---|
include I18nTranslation
|
||
bindtextdomain("cyborghood", {:path => Config::L10N_DIR, :charset => "UTF-8"})
|
||
|
||
attr_reader :name
|
||
attr_reader :name, :services
|
||
|
||
def initialize
|
||
@name = self.class.name.split("::").last
|
||
def initialize(services = nil)
|
||
@services = services
|
||
|
||
@name = self.class.name.split("::").last
|
||
@config = Config.instance
|
||
|
||
# setup logs
|
Also available in: Unified diff
[evol] MapMaker API and internals rework §1, going forward to Dependency Injection using Needle