Commit 8d6fe82e authored by Julian Kornberger's avatar Julian Kornberger

first commit

parents
/.project
/.bundle
/data
/tmp
2.0.0-p195
GEM
remote: https://rubygems.org/
specs:
zonefile (1.04)
PLATFORMS
ruby
DEPENDENCIES
zonefile
Copyright (C) 2013 Digineo GmbH, Germany
http://www.digineo.de/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
DNS Git
=======
Run your own DNS servers and manage your zones easily with Git.
This piece of **free** software gives you the ability to describe your zone files in a **simple DSL** (Domain Specific Language) with **templates** and store everything in a **Git repository**.
Every time you push your changes, a hook generates all zone files and increases serial numbers, if necessary.
We have been inspired by [LuaDNS](http://www.luadns.com/).
DNS Git has been tested with:
* [PowerDNS](https://www.powerdns.com/)
Installation
------------
Please ensure your have Git and a current version of Ruby (1.9.3 or 2.0.0) installed.
On Debian/Ubuntu you can do:
apt-get install -y git-core curl
curl -L https://get.rvm.io | bash -s stable --ruby=2.0.0
Then clone the repository and install the required libraries using bundler.
git clone git://github.com/digineo/dnsgit /opt/dnsgit
cd /opt/dnsgit
bundle install
Finally, just generate a sample configuration.
bin/init
Configuration
-------------
Run these steps locally on your own machine:
git clone ssh://root@your-server/opt/dnsgit/data dns-config
cd dns-config
... do some changes ...
git add -A
git commit -m "my commit message"
git push
### Examples
Take a look at the [/lib/example](https://github.com/digineo/dnsgit/tree/master/lib/example) folder and the [tests](https://github.com/digineo/dnsgit/tree/master/tests/zone_test.rb).
#!/bin/bash -e
basedir="`dirname $0`/../.."
# Extract the new commit id
ref=`cat /dev/stdin | awk '{ print $2 }'`
# Export working copy
export GIT_WORK_TREE=$basedir/tmp/cache
mkdir -p $GIT_WORK_TREE
GIT_WORK_TREE=$basedir/tmp/cache git checkout -f --quiet $ref
# This loads RVM into a shell session.
for file in /usr/local/lib/rvm $HOME/.rvm/scripts/rvm; do
if [[ -s "$file" ]]; then
source $file;
fi
done
#ruby --version
# Generate Zones
$basedir/bin/run.rb
#!/bin/bash -e
basedir=$( readlink -f $( dirname $0 )/.. )
repo=$basedir/data
tmp=$basedir/tmp/init
if [ -e $repo ]; then
echo $repo does already exist
exit 1
fi
# Create a temporary directoy and fill working copy with examples
mkdir -p $tmp
cd $tmp
cp -R $basedir/lib/example/* ./
cp $basedir/lib/example/.gitignore ./
# Initialize repository
git init --quiet
git add -A
git commit -m "Sample configuration" --quiet
# Symlink hooks
rm -rf .git/hooks
ln -s ../bin/hooks .git/
# Move bare repository and remove working copy
mv $tmp/.git $repo
rm -rf $tmp
echo Please clone and update the configuration:
echo \$ git clone $repo dns-config
#!/usr/bin/env ruby
require File.expand_path('../../lib/environment', __FILE__)
generator = ZoneGenerator.new(BASEDIR)
generator.generate
generator.deploy
require 'rubygems'
require 'bundler/setup'
require 'zonefile'
require 'yaml'
BASEDIR = File.expand_path('../..', __FILE__)
require "#{BASEDIR}/lib/hash_ext"
require "#{BASEDIR}/lib/zone"
require "#{BASEDIR}/lib/zone_generator"
*~
.*
!.gitignore
# PowerDNS paths
named_conf: /etc/powerdns/named.conf
zones_dir: /var/lib/powerdns/zones
# Command to run after push
execute: '/usr/bin/pdns_control reload'
# SOA Record
# Recommendations: http://www.ripe.net/ripe/docs/ripe-203
soa:
primary: "ns1.example.com."
email: "webmaster@example.com"
ttl: "1H"
refresh: "1D"
retry: "2H"
expire: "1W"
minimumTTL: "2D"
# NS records
ns "ns1.example.com."
ns "ns2.example.de."
ns "ns3.example.de."
template "example-dns"
# A records
a "a.ns", "192.168.1.2", 3600
a "b.ns", "192.168.1.3", 3600
a "mx1", "192.168.1.11"
a "mx2", "192.168.1.12"
a "sipserver", "192.168.1.200"
# AAAA records
aaaa "2001:4860:4860::8888"
# MX records
mx "mx1", 10
mx "mx2", 20
# CNAME records
cname "www", "@"
txt "google-site-verification=vEj1ZcGtXeM_UEjnCqQEhxPSqkS9IQ4PBFuh48FP8o4"
# SRV records
srv :sip, :tcp, "sipserver.example.net.", 5060
# Wildcard records
a "*.user", "192.168.1.100"
mx "*.user", "mail"
class Hash
def symbolize_keys!
keys.each do |key|
self[(key.to_sym rescue key) || key] = delete(key)
end
self
end
def deep_symbolize_keys!
symbolize_keys!
# symbolize each hash in .values
values.each{|h| h.deep_symbolize_keys! if h.is_a?(Hash) }
# symbolize each hash inside an array in .values
values.select{|v| v.is_a?(Array) }.flatten.each{|h| h.deep_symbolize_keys! if h.is_a?(Hash) }
self
end
end
\ No newline at end of file
class Zone
attr_reader :zonefile
def initialize(domain, template_dir)
@domain = domain
@zonefile = Zonefile.new("","output/#{domain}", domain)
@template_dir = template_dir
end
def template(name)
eval_file "#{@template_dir}/#{name}.rb"
end
# 1.2.3.4 - host
# 1.2.3.4, 600 - host with TTL
# www, 1.2.3.4, 600 - name, host and TTL
def a(*args)
if [String,String,String] == args[0..2].map(&:class)
# name, ipv4 and ipv6
name = args.shift
ipv4 = args.shift
ipv6 = args.shift
a_record :a, name, ipv4, *args
a_record :a4, name, ipv6, *args
else
a_record :a, *args
end
end
def aaaa(*args)
a_record :a4, *args
end
def a_record(type, *args)
ttl = extract_ttl! args
host = args.pop
name = args.pop || '@'
push type, name, ttl, host: host
end
# mx - host with default priority (10)
# mx, 15 - host and priority
# mx, 15, 600 - host, priority and TTL
# name, mx, 15 - name, host, priority
# name, mx, 15, 600 - name, host, priority and TTL
def mx(*args)
if args[1].is_a?(String)
# name and host given
name = args.shift
host = args.shift
else
# only host given
host = args.shift || '@'
name = '@'
end
pri = args.shift || 10
ttl = args.shift
push :mx, name, ttl, host: host, pri: pri
end
# ns1.example.com. - host
# ns1.example.com., 600 - host with TTL
def ns(*args)
ttl = extract_ttl! args
host = args.pop
name = args.pop || '@'
push :ns, name, ttl, host: host
end
def cname(name, *args)
ttl = extract_ttl! args
push :cname, name, ttl, host: (args.pop || "@")
end
def srv(*args)
options = extract_options! args
name = "." << args.shift if args[0].is_a?(String)
raise ArgumentError, "wrong number of arguments" unless (4..5).include?(args.count)
service = args.shift
protocol = args.shift
host = args.shift
port = args.shift
ttl = extract_ttl! args
options.each do |key,val|
case key
when :pri, :weight
raise ArgumentError, "invalid #{key}: #{val}" if val.to_s !~ /^\d+$/
else
raise ArgumentError, "unknown option: #{key}"
end
end
# default values
options[:pri] ||= 10
options[:weight] ||= 0
push :srv, "_#{service}._#{protocol}#{name}", ttl, options.merge(host: host, port: port)
end
def txt(*args)
ttl = extract_ttl! args
text = args.pop
name = args.pop || '@'
push :txt, name, ttl, text: text
end
# name in not-reversed order
def ptr(name, host, ttl=nil)
host = "#{host}." if host[-1] != '.'
push :ptr, name, ttl, host: host
end
def ptr6(name, *args)
raise ArgumentError, "no double colon allowed" if name.include?("::")
# left fill blocks with zeros, reverse order all characters and join them with points
ptr name.split(":").map{|b| b.rjust(4,"0") }.join.reverse.split("").join("."), *args
end
protected
# evaluates a file
def eval_file(file)
instance_eval File.read(file), file
end
def push(type, name, ttl, options={})
@zonefile.send(type) << {class: 'IN', name: name, ttl: ttl, }.merge(options)
end
# extracts the last argument if it is a Hash
def extract_options!(args)
args.last.is_a?(Hash) ? args.pop : {}
end
# extracts the last argument if it is a Fixnum
def extract_ttl!(args)
args.pop if args.last.is_a?(Fixnum)
end
end
\ No newline at end of file
class ZoneGenerator
def initialize(basedir)
@generated = "#{basedir}/tmp/generated"
@workspace = "#{basedir}/tmp/cache"
@zones_dir = "#{@workspace}/zones"
@template_dir = "#{@workspace}/templates"
@tmp_named = "#{@generated}/named.conf"
@tmp_zones = "#{@generated}/zones"
@config = YAML.load_file("#{@workspace}/config.yaml")
@config.deep_symbolize_keys!
@soa = {
origin: "@",
ttl: "86400",
primary: "example.com.",
email: "hostmaster@example.com",
refresh: "8H",
retry: "2H",
expire: "1W",
minimumTTL: "11h"
}.merge(@config[:soa])
# Rewrite email address
if (email = @soa[:email]).include?("@")
@soa[:email] = email.sub("@",".") << "."
end
FileUtils.rm_rf @generated # Tote Zonen-Definitionen brauchen wir nicht.
FileUtils.mkdir_p @generated
end
# Generates all zones
def generate
File.open(@tmp_named,"w") do |f|
Dir.glob("#{@zones_dir}/**/*.rb").sort.each do |file|
domain = File.basename(file).sub(/\.rb$/,"")
generate_zone(file, domain)
f.puts "zone \"#{domain}\" IN { type master; file \"#{@config[:zones_dir]}/#{domain}\"; };"
end
end
end
# Generates a single zone file
def generate_zone(file, domain)
zone = Zone.new(domain, @template_dir)
zone.send :eval_file, file
new_zonefile = zone.zonefile
new_zonefile.soa.merge! @soa
# path to the deployed version
old_file = "#{@config[:zones_dir]}/#{domain}"
# is there already a deployed version?
if File.exists?(old_file)
# parse the deployed version
old_output = File.read(old_file)
old_zonefile = Zonefile.new(old_output)
new_zonefile.soa[:serial] = old_zonefile.soa[:serial]
# content of the new version
new_output = new_zonefile.output
# has anything changed?
if new_output != old_output
puts "#{domain} has been updated"
# increment serial
new_zonefile.new_serial
new_output = new_zonefile.output
end
else
# zone has not existed before
puts "#{domain} has been created"
new_zonefile.new_serial
new_output = new_zonefile.output
end
# Write new zonefile
output_file_path = "#{@tmp_zones}/#{domain}"
FileUtils.mkdir_p File.dirname(output_file_path)
File.open(output_file_path, "w"){|f| f.write new_output }
end
def deploy
# Remove zones directory
FileUtils.rm_rf @config[:zones_dir]
FileUtils.copy @tmp_named, @config[:named_conf]
FileUtils.copy_entry @tmp_zones, @config[:zones_dir]
cmd = @config[:execute]
print "Executing '#{cmd}' ... "
out = `#{cmd}`
puts "done"
if $?.to_i != 0
raise out
end
end
end
\ No newline at end of file
require File.expand_path('../../lib/environment', __FILE__)
require 'minitest/autorun'
require 'test_helper'
describe Zone do
subject do
Zone.new("example.com",nil)
end
describe "a record" do
it "should create host" do
subject.a "127.0.0.1"
subject.zonefile.a.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :host=>"127.0.0.1"}]
end
it "should create host, ttl" do
subject.a "127.0.0.1", 600
subject.zonefile.a.must_equal [{:class=>"IN", :name=>"@", :ttl=>600, :host=>"127.0.0.1"}]
end
it "should create name, host, ttl" do
subject.a "www", "127.0.0.1", 600
subject.zonefile.a.must_equal [{:class=>"IN", :name=>"www", :ttl=>600, :host=>"127.0.0.1"}]
end
it "should create name with ipv4 and ipv6" do
subject.a "www", "127.0.0.1", "::ffff:7f00:1"
subject.zonefile.a.must_equal [{:class=>"IN", :name=>"www", :ttl=>nil, :host=>"127.0.0.1"}]
subject.zonefile.a4.must_equal [{:class=>"IN", :name=>"www", :ttl=>nil, :host=>"::ffff:7f00:1"}]
end
it "should create name with ipv4, ipv6 and TTL" do
subject.a "www", "127.0.0.1", "::ffff:7f00:1", 600
subject.zonefile.a.must_equal [{:class=>"IN", :name=>"www", :ttl=>600, :host=>"127.0.0.1"}]
subject.zonefile.a4.must_equal [{:class=>"IN", :name=>"www", :ttl=>600, :host=>"::ffff:7f00:1"}]
end
end
describe "cname record" do
it "without args" do
assert_raises ArgumentError do
subject.cname
end
end
it "with name" do
subject.cname 'www'
subject.zonefile.cname.must_equal [{:class=>"IN", :name=>"www", :ttl=>nil, :host=>"@"}]
end
it "with name, host" do
subject.cname 'www', "other-server."
subject.zonefile.cname.must_equal [{:class=>"IN", :name=>"www", :ttl=>nil, :host=>"other-server."}]
end
it "with name, ttl" do
subject.cname 'www', 600
subject.zonefile.cname.must_equal [{:class=>"IN", :name=>"www", :ttl=>600, :host=>"@"}]
end
it "with name, ttl, host" do
subject.cname 'www', "other-server.", 600
subject.zonefile.cname.must_equal [{:class=>"IN", :name=>"www", :ttl=>600, :host=>"other-server."}]
end
end
describe "mx record" do
it "should create without args" do
subject.mx
subject.zonefile.mx.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :host=>"@", :pri=>10}]
end
it "should create with host" do
subject.mx "mail"
subject.zonefile.mx.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :host=>"mail", :pri=>10}]
end
it "should create with host, priority" do
subject.mx "mail", 20
subject.zonefile.mx.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :host=>"mail", :pri=>20}]
end
it "should create with host, priority, ttl " do
subject.mx "mail", 20, 600
subject.zonefile.mx.must_equal [{:class=>"IN", :name=>"@", :ttl=>600, :host=>"mail", :pri=>20}]
end
end
describe "srv record" do
it "without port" do
assert_raises ArgumentError do
subject.srv :ldap, :tcp, 'ldap01'
end
end
it "should create srv record" do
subject.srv :ldap, :tcp, 'ldap01', 389
subject.zonefile.srv.must_equal [{:class=>"IN", :name=>"_ldap._tcp", :ttl=>nil, :pri=>10, :weight=>0, :host=>"ldap01", port: 389}]
end
it "should create srv record with ttl" do
subject.srv :ldap, :tcp, 'ldap01', 389, 600
subject.zonefile.srv.must_equal [{:class=>"IN", :name=>"_ldap._tcp", :ttl=>600, :pri=>10, :weight=>0, :host=>"ldap01", port: 389}]
end
it "should create srv record with pri and weight" do
subject.srv :ldap, :tcp, 'ldap01', 389, pri: 15, weight: 3
subject.zonefile.srv.must_equal [{:class=>"IN", :name=>"_ldap._tcp", :ttl=>nil, :pri=>15, :weight=>3, :host=>"ldap01", port: 389}]
end
it "should create srv record with name" do
subject.srv "foo", :ldap, :tcp, 'ldap01', 389
subject.zonefile.srv.must_equal [{:class=>"IN", :name=>"_ldap._tcp.foo", :ttl=>nil, :pri=>10, :weight=>0, :host=>"ldap01", port: 389}]
end
end
describe "ptr record" do
it "should create ptr record" do
subject.ptr 127, "foobar.org"
subject.zonefile.ptr.must_equal [{:class=>"IN", :name=>127, :ttl=>nil, :host=>"foobar.org."}]
end
end
describe "ptr6 record" do
it "with a double colon" do
assert_raises ArgumentError do
subject.ptr6 "1319::1", "example.com"
end
end
it "should create ptr record" do
subject.ptr6 "1319:8a2e:0370:7344", "example.com"
subject.zonefile.ptr.must_equal [{:class=>"IN", :name=>"4.4.3.7.0.7.3.0.e.2.a.8.9.1.3.1", :ttl=>nil, :host=>"example.com."}]
end
it "should create ptr record with filled zeros" do
subject.ptr6 "1319:8a2e:70:1", "example.com"
subject.zonefile.ptr.must_equal [{:class=>"IN", :name=>"1.0.0.0.0.7.0.0.e.2.a.8.9.1.3.1", :ttl=>nil, :host=>"example.com."}]
end
end
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment