unbind 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 649241be7fdfaeb998b499334bef1325010744a7
4
+ data.tar.gz: c9f48654df4efc5eb9eaa539c57fca5b45e18a83
5
+ SHA512:
6
+ metadata.gz: 9a290a2d4c77f5dfe5f1414fb4c4d17bc26a750fd356a151e754f260b4fc626f39cc11e6a83963cff0e8aa61cc1b9cd3aa897d73d9d545c764fabbffdaf101eb
7
+ data.tar.gz: 8df5b75089aa332d0db6b1dd606f1de74187493aa0d92133fe4a5704a41f39b8b180d69058c874f17ffa1ff86a4471c3ce69a875bb188c560e4d7b85e77f81ec
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ .config
4
+ .yardoc
5
+ .DS_Store
6
+ Gemfile.lock
7
+ spec/tmp
8
+
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in unbind.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard :rspec do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Dennis Krupenik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # UNBIND
2
+
3
+ [![Code Climate](https://codeclimate.com/github/krupenik/unbind.png)](https://codeclimate.com/github/krupenik/unbind)
4
+ [![Build Status](https://travis-ci.org/krupenik/unbind.png?branch=master)](https://travis-ci.org/krupenik/unbind)
5
+
6
+ ISC BINDv9 config generator
7
+
8
+ ## Installation
9
+
10
+ gem install unbind
11
+
12
+ ## Usage
13
+
14
+ $ unbind [-c <config file>] command
15
+
16
+ Commands:
17
+ master generate named.conf for master
18
+ slave generate named.conf for slave
19
+ zone zone_name [view_name] generate zone file for view
20
+
21
+ Options:
22
+ -c config file to use
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/bin/unbind ADDED
@@ -0,0 +1,33 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "unbind"
5
+
6
+ options = {config: "/etc/unbind.conf"}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = <<-EOF
9
+ Usage: unbind [-c <config file>] command
10
+
11
+ Commands:
12
+ master generate named.conf for master
13
+ slave generate named.conf for slave
14
+ zone zone_name [view_name] generate zone file for view
15
+
16
+ Options:
17
+ -c config file to use
18
+ EOF
19
+
20
+ opts.on("-cCONFIG", "--config-file CONFIG", "Configuration file (default: #{options[:config]})") do |x|
21
+ options[:config] = x
22
+ end
23
+ end
24
+
25
+ opts.parse!
26
+ command = ARGV.shift
27
+
28
+ unless Unbind::Commands.include? command
29
+ abort opts.help
30
+ end
31
+
32
+ Unbind.load_config options[:config]
33
+ print Unbind.send(command, ARGV)
data/lib/unbind.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'yaml'
2
+
3
+ require 'unbind/core_ext/hash/keys'
4
+ require 'unbind/core_ext/string/inflections'
5
+
6
+ require 'unbind/version'
7
+ require 'unbind/view'
8
+ require 'unbind/zone'
9
+
10
+ module Unbind
11
+ Commands = %w(master slave zone).freeze
12
+
13
+ @config = {}
14
+
15
+ class << self
16
+ attr_reader :config, :geoip
17
+
18
+ def load_config config
19
+ clear_config
20
+
21
+ files = File.directory?(config) ? Dir["#{config}/**/*.conf"] : Dir[config]
22
+ raise "No files could be found (search path: #{config})" if files.empty?
23
+ files.each { |f| raise "File '#{f}' could not be loaded" unless load_file(f) }
24
+
25
+ prepare_config
26
+
27
+ true
28
+ end
29
+
30
+ def master(*)
31
+ views.map { |v| v.master }.join("\n")
32
+ end
33
+
34
+ def slave(*)
35
+ views.map { |v| v.slave }.join("\n")
36
+ end
37
+
38
+ def zone zone_names
39
+ zone_names.map { |z| Unbind::Zone.new(z, @config[:zones][z]).generate }.join("\n")
40
+ end
41
+
42
+ # private
43
+
44
+ def load_file f
45
+ load_string(File.read(File.expand_path(f)))
46
+ end
47
+
48
+ def load_string s
49
+ data = YAML.load(s)
50
+ raise "config data should be a hash" unless data.is_a? Hash
51
+ @config.merge!(data) and true
52
+ end
53
+
54
+ def clear_config
55
+ @config = {}
56
+ end
57
+
58
+ def prepare_config
59
+ symbolize_config
60
+ init_geoip
61
+ end
62
+
63
+ def init_geoip
64
+ if @config[:geoip_dat]
65
+ begin
66
+ require 'geoip'
67
+ @geoip = GeoIP.new(@config[:geoip_dat])
68
+ rescue LoadError
69
+ end
70
+ end
71
+ end
72
+
73
+ def symbolize_config
74
+ @config.symbolize_keys!
75
+
76
+ @config[:views] ||= {}
77
+ @config[:zones] ||= {}
78
+
79
+ @config[:zones].each do |k, v|
80
+ v.symbolize_keys!
81
+ v[:data].symbolize_keys!
82
+ end
83
+ end
84
+
85
+ def policies
86
+ @policies ||= @config[:zones].reduce([]) { |a, (name, config)| a + (config[:policies] || []) }.map { |policy_name|
87
+ Unbind::Policy.const_get(policy_name.camelize)
88
+ }
89
+ end
90
+
91
+ def zones
92
+ @zones ||= @config[:zones].map { |name, config| Unbind::Zone.new(name, config) }
93
+ end
94
+
95
+ def views
96
+ @views ||= @config[:views].map { |name, clients| Unbind::View.new(name, {clients: clients, zones: zones}) }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,140 @@
1
+ # grabbed from activesupport-4.0.1
2
+
3
+ class Hash
4
+ # Return a new hash with all keys converted using the block operation.
5
+ #
6
+ # hash = { name: 'Rob', age: '28' }
7
+ #
8
+ # hash.transform_keys{ |key| key.to_s.upcase }
9
+ # # => { "NAME" => "Rob", "AGE" => "28" }
10
+ def transform_keys
11
+ result = {}
12
+ each_key do |key|
13
+ result[yield(key)] = self[key]
14
+ end
15
+ result
16
+ end
17
+
18
+ # Destructively convert all keys using the block operations.
19
+ # Same as transform_keys but modifies +self+.
20
+ def transform_keys!
21
+ keys.each do |key|
22
+ self[yield(key)] = delete(key)
23
+ end
24
+ self
25
+ end
26
+
27
+ # Return a new hash with all keys converted to strings.
28
+ #
29
+ # hash = { name: 'Rob', age: '28' }
30
+ #
31
+ # hash.stringify_keys
32
+ # #=> { "name" => "Rob", "age" => "28" }
33
+ def stringify_keys
34
+ transform_keys{ |key| key.to_s }
35
+ end
36
+
37
+ # Destructively convert all keys to strings. Same as
38
+ # +stringify_keys+, but modifies +self+.
39
+ def stringify_keys!
40
+ transform_keys!{ |key| key.to_s }
41
+ end
42
+
43
+ # Return a new hash with all keys converted to symbols, as long as
44
+ # they respond to +to_sym+.
45
+ #
46
+ # hash = { 'name' => 'Rob', 'age' => '28' }
47
+ #
48
+ # hash.symbolize_keys
49
+ # #=> { name: "Rob", age: "28" }
50
+ def symbolize_keys
51
+ transform_keys{ |key| key.to_sym rescue key }
52
+ end
53
+ alias_method :to_options, :symbolize_keys
54
+
55
+ # Destructively convert all keys to symbols, as long as they respond
56
+ # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
57
+ def symbolize_keys!
58
+ transform_keys!{ |key| key.to_sym rescue key }
59
+ end
60
+ alias_method :to_options!, :symbolize_keys!
61
+
62
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
63
+ # on a mismatch. Note that keys are NOT treated indifferently, meaning if you
64
+ # use strings for keys but assert symbols as keys, this will fail.
65
+ #
66
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years"
67
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name"
68
+ # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
69
+ def assert_valid_keys(*valid_keys)
70
+ valid_keys.flatten!
71
+ each_key do |k|
72
+ raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k)
73
+ end
74
+ end
75
+
76
+ # Return a new hash with all keys converted by the block operation.
77
+ # This includes the keys from the root hash and from all
78
+ # nested hashes.
79
+ #
80
+ # hash = { person: { name: 'Rob', age: '28' } }
81
+ #
82
+ # hash.deep_transform_keys{ |key| key.to_s.upcase }
83
+ # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } }
84
+ def deep_transform_keys(&block)
85
+ result = {}
86
+ each do |key, value|
87
+ result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
88
+ end
89
+ result
90
+ end
91
+
92
+ # Destructively convert all keys by using the block operation.
93
+ # This includes the keys from the root hash and from all
94
+ # nested hashes.
95
+ def deep_transform_keys!(&block)
96
+ keys.each do |key|
97
+ value = delete(key)
98
+ self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
99
+ end
100
+ self
101
+ end
102
+
103
+ # Return a new hash with all keys converted to strings.
104
+ # This includes the keys from the root hash and from all
105
+ # nested hashes.
106
+ #
107
+ # hash = { person: { name: 'Rob', age: '28' } }
108
+ #
109
+ # hash.deep_stringify_keys
110
+ # # => { "person" => { "name" => "Rob", "age" => "28" } }
111
+ def deep_stringify_keys
112
+ deep_transform_keys{ |key| key.to_s }
113
+ end
114
+
115
+ # Destructively convert all keys to strings.
116
+ # This includes the keys from the root hash and from all
117
+ # nested hashes.
118
+ def deep_stringify_keys!
119
+ deep_transform_keys!{ |key| key.to_s }
120
+ end
121
+
122
+ # Return a new hash with all keys converted to symbols, as long as
123
+ # they respond to +to_sym+. This includes the keys from the root hash
124
+ # and from all nested hashes.
125
+ #
126
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
127
+ #
128
+ # hash.deep_symbolize_keys
129
+ # # => { person: { name: "Rob", age: "28" } }
130
+ def deep_symbolize_keys
131
+ deep_transform_keys{ |key| key.to_sym rescue key }
132
+ end
133
+
134
+ # Destructively convert all keys to symbols, as long as they respond
135
+ # to +to_sym+. This includes the keys from the root hash and from all
136
+ # nested hashes.
137
+ def deep_symbolize_keys!
138
+ deep_transform_keys!{ |key| key.to_sym rescue key }
139
+ end
140
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def camelize
3
+ self.split('_').map(&:capitalize).join('')
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Unbind
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,74 @@
1
+ module Unbind
2
+ class View
3
+ def self.expand_countries clients
4
+ clients.flat_map { |i|
5
+ if i.is_a?(Hash) && i.has_key?("countries")
6
+ i["countries"].map { |c| "country_#{c.upcase}" }
7
+ else
8
+ i
9
+ end
10
+ }
11
+ end
12
+
13
+ def initialize name, config
14
+ raise "view must have a name" unless name.is_a?(String) && 0 < name.length
15
+ raise "config should include clients and zones lists" unless
16
+ config.is_a?(Hash) && [:clients, :zones].all? { |k| config.has_key?(k) && config[k].is_a?(Array) }
17
+
18
+ @name = name
19
+ @clients = config[:clients]
20
+ @zones = config[:zones]
21
+ end
22
+
23
+ def slave
24
+ ["view \"#{@name}\" {", match_clients, zones(:slave), "};\n"].join("\n")
25
+ end
26
+
27
+ def master
28
+ ["view \"#{@name}\" {", match_clients, servers, view_settings(:master), zones(:master), "};\n"].join("\n")
29
+ end
30
+
31
+ def clients
32
+ self.class.expand_countries(@clients)
33
+ end
34
+
35
+ private
36
+
37
+ def slaves
38
+ @zones.reduce([]) { |a, e| a + e.slaves }
39
+ end
40
+
41
+ def match_clients
42
+ "match-clients { key #{@name}; !tsig_keys; #{clients.join("; ")}; };"
43
+ end
44
+
45
+ def file zone
46
+ "file \"pri/#{zone.name}/#{@name}.zone\";"
47
+ end
48
+
49
+ def masters zone
50
+ "masters { %s key %s; };" % [zone.master, @name]
51
+ end
52
+
53
+ def servers
54
+ slaves.map { |slave|
55
+ "server #{slave} { keys #{@name}; };"
56
+ }
57
+ end
58
+
59
+ def view_settings role
60
+ case role
61
+ when :master
62
+ "allow-transfer { keys #{@name}; };\nnotify yes;"
63
+ end
64
+ end
65
+
66
+ def zones role
67
+ @zones.flat_map { |zone|
68
+ ([zone.name] + zone.aliases).flat_map { |zone_name|
69
+ "zone \"#{zone_name}\" { type #{role}; #{:master == role ? file(zone) : masters(zone) } };"
70
+ }
71
+ }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+ module Unbind
2
+ class Zone
3
+ TTL = 600
4
+
5
+ attr_reader :aliases, :master, :name, :slaves, :ttl, :version
6
+
7
+ def initialize name, config
8
+ raise "zone should have a valid name (given: #{name})" unless name =~ /\A[\w\-\.]+\z/
9
+
10
+ @name = name
11
+
12
+ raise 'zone config should include data' unless
13
+ config.is_a?(Hash) && config.has_key?(:data) && config[:data].is_a?(Hash)
14
+
15
+ @config = config
16
+ @data = @config[:data]
17
+
18
+ # raise 'zone data should include name and mail servers and at least one A record' unless
19
+ # [:ns, :mx, :a].all? { |k| @data.has_key?(k) && !@data[k].empty? }
20
+
21
+ sanitize_ttl
22
+ sanitize_version
23
+ assign_optional_data
24
+ end
25
+
26
+ def generate
27
+ [header, essentials, resources, nil].join("\n")
28
+ end
29
+
30
+ private
31
+
32
+ def assign_optional_data
33
+ [:aliases, :master, :slaves].each do |i|
34
+ instance_variable_set(:"@#{i}", @config[i] || [])
35
+ end
36
+ end
37
+
38
+ def sanitize_ttl
39
+ @ttl = @config[:ttl].to_i
40
+ @ttl = TTL if @ttl <= 0
41
+ end
42
+
43
+ def sanitize_version
44
+ @version = @config[:version].to_i
45
+ @version = Time.now.utc.strftime("%Y%m%d%H%M") if @version <= 0
46
+ end
47
+
48
+ def header
49
+ [
50
+ "$TTL #{ttl}",
51
+ "@ SOA ns0 root (#{version} 1d 10m 2w 10m)",
52
+ ]
53
+ end
54
+
55
+ def essentials
56
+ Array(@data[:ns]).map { |name| "@ NS #{name}" } +
57
+ Array(@data[:mx]).map.with_index { |name, prio| "@ MX #{prio+1} #{name}" }
58
+ end
59
+
60
+ def resources
61
+ (@data.keys - [:ns, :mx]).reduce([]) { |a, type|
62
+ a + @data[type].reduce([]) { |a, (names, addresses)|
63
+ a + names.split(/\s*,\s*/).reduce([]) { |a, name|
64
+ a + Array(addresses).reduce([]) { |a, address|
65
+ a + ["#{name} #{type.upcase} #{address}"]
66
+ }
67
+ }
68
+ }
69
+ }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ require 'unbind'
2
+ require 'spec_helper'
3
+
4
+ describe Unbind do
5
+ include_full_config
6
+
7
+ describe '.slave' do
8
+ it "generates config for slaves" do
9
+ expect(subject.slave).to eq(<<EOF
10
+ view "internal" {
11
+ match-clients { key internal; !tsig_keys; 127.0.0.0/8; 10.0.0.0/24; };
12
+ zone "zone.ua" { type slave; masters { 10.0.0.1 key internal; }; };
13
+ zone "zone.uk" { type slave; masters { 10.0.0.1 key internal; }; };
14
+ zone "zone.us" { type slave; masters { 10.0.0.1 key internal; }; };
15
+ };
16
+
17
+ view "external" {
18
+ match-clients { key external; !tsig_keys; country_UA; country_UK; country_US; country_TT; };
19
+ zone "zone.ua" { type slave; masters { 10.0.0.1 key external; }; };
20
+ zone "zone.uk" { type slave; masters { 10.0.0.1 key external; }; };
21
+ zone "zone.us" { type slave; masters { 10.0.0.1 key external; }; };
22
+ };
23
+ EOF
24
+ )
25
+ end
26
+ end
27
+
28
+ describe '.master' do
29
+ it "generates config for the master" do
30
+ expect(subject.master).to eq(<<EOF
31
+ view "internal" {
32
+ match-clients { key internal; !tsig_keys; 127.0.0.0/8; 10.0.0.0/24; };
33
+ server 192.168.0.2 { keys internal; };
34
+ server 192.168.0.3 { keys internal; };
35
+ allow-transfer { keys internal; };
36
+ notify yes;
37
+ zone "zone.ua" { type master; file "pri/zone.ua/internal.zone"; };
38
+ zone "zone.uk" { type master; file "pri/zone.ua/internal.zone"; };
39
+ zone "zone.us" { type master; file "pri/zone.ua/internal.zone"; };
40
+ };
41
+
42
+ view "external" {
43
+ match-clients { key external; !tsig_keys; country_UA; country_UK; country_US; country_TT; };
44
+ server 192.168.0.2 { keys external; };
45
+ server 192.168.0.3 { keys external; };
46
+ allow-transfer { keys external; };
47
+ notify yes;
48
+ zone "zone.ua" { type master; file "pri/zone.ua/external.zone"; };
49
+ zone "zone.uk" { type master; file "pri/zone.ua/external.zone"; };
50
+ zone "zone.us" { type master; file "pri/zone.ua/external.zone"; };
51
+ };
52
+ EOF
53
+ )
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ require 'unbind/view'
2
+ require 'spec_helper'
3
+
4
+ describe Unbind::View do
5
+ include_view_definition
6
+
7
+ describe "#slave" do
8
+ it "generates the view for a slave" do
9
+ expect(subject.slave).to eq(<<EOF
10
+ view "test" {
11
+ match-clients { key test; !tsig_keys; country_UA; country_UK; country_US; };
12
+ zone "zone.ua" { type slave; masters { 10.0.0.1 key test; }; };
13
+ zone "zone.uk" { type slave; masters { 10.0.0.1 key test; }; };
14
+ zone "zone.us" { type slave; masters { 10.0.0.1 key test; }; };
15
+ };
16
+ EOF
17
+ )
18
+ end
19
+ end
20
+
21
+ describe "#master" do
22
+ it "generates the view for the master" do
23
+ expect(subject.master).to eq(<<EOF
24
+ view "test" {
25
+ match-clients { key test; !tsig_keys; country_UA; country_UK; country_US; };
26
+ server 10.0.0.2 { keys test; };
27
+ server 10.0.0.3 { keys test; };
28
+ allow-transfer { keys test; };
29
+ notify yes;
30
+ zone "zone.ua" { type master; file "pri/zone.ua/test.zone"; };
31
+ zone "zone.uk" { type master; file "pri/zone.ua/test.zone"; };
32
+ zone "zone.us" { type master; file "pri/zone.ua/test.zone"; };
33
+ };
34
+ EOF
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ require 'unbind/zone'
2
+ require 'spec_helper'
3
+
4
+ describe Unbind::Zone do
5
+ include_zone_definition
6
+
7
+ describe "#generate" do
8
+ it "generates zone file content" do
9
+ expect(subject.generate).to eq(<<EOF
10
+ $TTL #{subject.ttl}
11
+ @ SOA ns0 root (#{subject.version} 1d 10m 2w 10m)
12
+ @ NS ns1
13
+ @ NS ns2
14
+ @ MX 1 mail
15
+ @ A 10.0.0.1
16
+ @ A 10.0.0.2
17
+ @ A 10.0.0.3
18
+ * A 10.0.0.1
19
+ * A 10.0.0.2
20
+ * A 10.0.0.3
21
+ mail A 10.0.0.1
22
+ mail A 10.0.0.2
23
+ ns1 A 10.0.0.1
24
+ ns2 A 10.0.0.2
25
+ @ TXT "txt data"
26
+ imap CNAME mail
27
+ _xmpp-client._tcp SRV 5222 0 5 .
28
+ EOF
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ def include_zone_definition
2
+ require 'unbind/zone'
3
+
4
+ zone = Unbind::Zone.new("zone.ua", {
5
+ master: '10.0.0.1',
6
+ slaves: %w(10.0.0.2 10.0.0.3),
7
+ aliases: %w(zone.uk zone.us),
8
+ data: {
9
+ ns: %w(ns1 ns2),
10
+ mx: 'mail',
11
+ a: {
12
+ '@, *' => %w(10.0.0.1 10.0.0.2 10.0.0.3),
13
+ 'mail' => %w(10.0.0.1 10.0.0.2),
14
+ 'ns1' => '10.0.0.1',
15
+ 'ns2' => '10.0.0.2',
16
+ },
17
+ txt: {
18
+ '@' => '"txt data"',
19
+ },
20
+ cname: {
21
+ 'imap' => 'mail',
22
+ },
23
+ srv: {
24
+ '_xmpp-client._tcp' => '5222 0 5 .'
25
+ },
26
+ },
27
+ })
28
+
29
+ @zone = zone
30
+
31
+ subject(:zone) { zone }
32
+ end
33
+
34
+ def include_view_definition
35
+ include_zone_definition
36
+
37
+ require 'unbind/view'
38
+
39
+ view = Unbind::View.new("test", {
40
+ clients: [{"countries" => %w(ua uk us)}],
41
+ zones: [@zone],
42
+ })
43
+
44
+ subject(:view) { view }
45
+ end
46
+
47
+ def include_full_config
48
+ Unbind.instance_variable_set(:@config, {
49
+ views: {
50
+ 'internal' => ['127.0.0.0/8', '10.0.0.0/24'],
51
+ 'external' => [{
52
+ 'countries' => ['ua', 'uk', 'us', 'tt'],
53
+ }],
54
+ },
55
+ zones: {
56
+ 'zone.ua' => {
57
+ master: '10.0.0.1',
58
+ slaves: ['192.168.0.2', '192.168.0.3'],
59
+ aliases: ['zone.uk', 'zone.us'],
60
+ data: {
61
+ ns: ['ns1', 'ns2'],
62
+ mx: ['mx1'],
63
+ a: {
64
+ 'ns1' => '192.168.0.2',
65
+ 'ns2' => '192.168.0.3',
66
+ 'mx1' => '192.168.0.2',
67
+ '@, *' => ['192.168.0.2', '192.168.0.3'],
68
+ },
69
+ },
70
+ },
71
+ },
72
+ })
73
+
74
+ Unbind.send :prepare_config
75
+
76
+ subject { Unbind }
77
+ end
@@ -0,0 +1,18 @@
1
+ require 'unbind/core_ext/hash/keys'
2
+ require 'unbind/core_ext/string/inflections'
3
+
4
+ describe Hash do
5
+ describe '#symbolize_keys' do
6
+ it 'converts string keys to symbols' do
7
+ expect({'symbol' => ''}.symbolize_keys).to eq({symbol: ''})
8
+ end
9
+ end
10
+ end
11
+
12
+ describe String do
13
+ describe '#camelize' do
14
+ it 'converts underscored_string to CamelCase' do
15
+ expect('camel_case_test'.camelize).to eq('CamelCaseTest')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,48 @@
1
+ require 'unbind'
2
+ require 'tempfile'
3
+ require 'spec_helper'
4
+
5
+ describe Unbind do
6
+ describe ".load_string" do
7
+ it "rejects non-hashes" do
8
+ expect { described_class.send(:load_string, nil) }.to raise_error
9
+ expect { described_class.send(:load_string, "") }.to raise_error
10
+ expect { described_class.send(:load_string, "---\n") }.to raise_error
11
+ expect { described_class.send(:load_string, "[]") }.to raise_error
12
+ end
13
+
14
+ it "accepts hashes" do
15
+ expect(described_class.send(:load_string, "{}")).to be_true
16
+ expect(described_class.send(:load_string, "--- {}\n")).to be_true
17
+ end
18
+ end
19
+
20
+ describe ".load_config" do
21
+ let(:tmp_conf) { Tempfile.new(described_class.to_s) }
22
+
23
+ context "with valid file" do
24
+ before { tmp_conf.write("{}"); tmp_conf.close }
25
+ after { tmp_conf.unlink }
26
+
27
+ it "reads config if config file exists" do
28
+ expect(described_class.load_config(tmp_conf.path)).to be_true
29
+ end
30
+ end
31
+
32
+ context "without file" do
33
+ it "raises meaningful error if config file does not exist" do
34
+ expect { described_class.load_config(tmp_conf.path) }.to raise_error
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'with config' do
40
+ include_full_config
41
+
42
+ describe "#zones" do
43
+ it 'collects and caches configured zones' do
44
+ expect((subject.send(:zones)).length).to eq(1)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,76 @@
1
+ require 'unbind/view'
2
+ require 'spec_helper'
3
+
4
+ describe Unbind::View do
5
+ describe ".expand_countries" do
6
+ it "expands countries list" do
7
+ expect(described_class.expand_countries([{"countries" => %w(ua uk us)}])).to match_array([
8
+ 'country_UA', 'country_UK', 'country_US'
9
+ ])
10
+ end
11
+ end
12
+
13
+ describe "#new" do
14
+ let(:valid_name) { 'a' }
15
+ let(:valid_data) { {clients: [], zones: []} }
16
+
17
+ it "requires valid name" do
18
+ expect { described_class.new(nil, valid_data) }.to raise_error
19
+ expect { described_class.new('', valid_data) }.to raise_error
20
+ end
21
+
22
+ it "requires valid data" do
23
+ expect { described_class.new(valid_name, {clients: nil, zones: []}) }.to raise_error
24
+ expect { described_class.new(valid_name, {clients: [], zones: nil}) }.to raise_error
25
+ end
26
+
27
+ it "requires both valid name and valid data" do
28
+ expect { described_class.new() }.to raise_error
29
+ expect { described_class.new(nil, nil) }.to raise_error
30
+ expect { described_class.new(valid_name, valid_data) }.not_to raise_error
31
+ end
32
+ end
33
+
34
+ context "generators" do
35
+ include_view_definition
36
+
37
+ describe "#match_clients" do
38
+ it "generates match-clients block" do
39
+ expect(view.send :match_clients).to eq("match-clients { key test; !tsig_keys; country_UA; country_UK; country_US; };")
40
+ end
41
+ end
42
+
43
+ describe "#servers" do
44
+ it "generates servers list" do
45
+ expect(view.send :servers).to match_array([
46
+ "server 10.0.0.2 { keys test; };",
47
+ "server 10.0.0.3 { keys test; };",
48
+ ])
49
+ end
50
+ end
51
+
52
+ describe "#view_settings" do
53
+ it "master should allow transfer with the key and notify" do
54
+ expect(view.send :view_settings, :master).to eq("allow-transfer { keys test; };\nnotify yes;")
55
+ end
56
+ end
57
+
58
+ describe "#zones" do
59
+ it "generates zones list for a slave" do
60
+ expect(view.send :zones, :slave).to match_array([
61
+ "zone \"zone.ua\" { type slave; masters { 10.0.0.1 key test; }; };",
62
+ "zone \"zone.uk\" { type slave; masters { 10.0.0.1 key test; }; };",
63
+ "zone \"zone.us\" { type slave; masters { 10.0.0.1 key test; }; };",
64
+ ])
65
+ end
66
+
67
+ it "generates zones list for the master" do
68
+ expect(view.send :zones, :master).to match_array([
69
+ "zone \"zone.ua\" { type master; file \"pri/zone.ua/test.zone\"; };",
70
+ "zone \"zone.uk\" { type master; file \"pri/zone.ua/test.zone\"; };",
71
+ "zone \"zone.us\" { type master; file \"pri/zone.ua/test.zone\"; };",
72
+ ])
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,66 @@
1
+ require 'unbind/zone'
2
+ require 'spec_helper'
3
+
4
+ describe Unbind::Zone do
5
+ let(:valid_name) { 'a' }
6
+ let(:valid_data) { {data: {ns: '.', mx: '.', a: {'' => ''}}} }
7
+
8
+ describe '#new' do
9
+ it 'requires a name' do
10
+ expect { described_class.new() }.to raise_error
11
+ expect { described_class.new(nil, valid_data) }.to raise_error
12
+ expect { described_class.new('', valid_data) }.to raise_error
13
+ expect { described_class.new(valid_name, valid_data) }.not_to raise_error
14
+ end
15
+
16
+ it 'requires essentials: ns, mx, a' do
17
+ expect { described_class.new(valid_name) }.to raise_error
18
+ expect { described_class.new(valid_name, nil) }.to raise_error
19
+ expect { described_class.new(valid_name, {}) }.to raise_error
20
+ expect { described_class.new(valid_name, valid_data) }.not_to raise_error
21
+ end
22
+ end
23
+
24
+ context 'generators' do
25
+ include_zone_definition
26
+
27
+ describe '#header' do
28
+ it 'generates zone header' do
29
+ expect(zone.send :header).to match_array([
30
+ "$TTL #{zone.ttl}",
31
+ "@ SOA ns0 root (#{zone.version} 1d 10m 2w 10m)",
32
+ ])
33
+ end
34
+ end
35
+
36
+ describe '#essentials' do
37
+ it 'generates zone essentials' do
38
+ expect(zone.send :essentials).to match_array([
39
+ '@ NS ns1',
40
+ '@ NS ns2',
41
+ '@ MX 1 mail',
42
+ ])
43
+ end
44
+ end
45
+
46
+ describe '#resources' do
47
+ it 'generates zone resources' do
48
+ expect(zone.send :resources).to match_array([
49
+ '@ A 10.0.0.1',
50
+ '@ A 10.0.0.2',
51
+ '@ A 10.0.0.3',
52
+ '* A 10.0.0.1',
53
+ '* A 10.0.0.2',
54
+ '* A 10.0.0.3',
55
+ 'mail A 10.0.0.1',
56
+ 'mail A 10.0.0.2',
57
+ 'ns1 A 10.0.0.1',
58
+ 'ns2 A 10.0.0.2',
59
+ '@ TXT "txt data"',
60
+ 'imap CNAME mail',
61
+ "_xmpp-client._tcp SRV 5222 0 5 ."
62
+ ])
63
+ end
64
+ end
65
+ end
66
+ end
data/unbind.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'unbind/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "unbind"
7
+ spec.version = Unbind::VERSION
8
+ spec.authors = ["Dennis Krupenik"]
9
+ spec.email = ["dennis@krupenik.com"]
10
+ spec.description = %q{ISC BINDv9 config generator}
11
+ spec.summary = %q{ISC BINDv9 config generator}
12
+ spec.homepage = "https://github.com/krupenik/unbind"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "rake"
21
+ spec.add_development_dependency "rspec"
22
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unbind
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dennis Krupenik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: ISC BINDv9 config generator
42
+ email:
43
+ - dennis@krupenik.com
44
+ executables:
45
+ - unbind
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - .gitignore
50
+ - .travis.yml
51
+ - Gemfile
52
+ - Guardfile
53
+ - LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - bin/unbind
57
+ - lib/unbind.rb
58
+ - lib/unbind/core_ext/hash/keys.rb
59
+ - lib/unbind/core_ext/string/inflections.rb
60
+ - lib/unbind/version.rb
61
+ - lib/unbind/view.rb
62
+ - lib/unbind/zone.rb
63
+ - spec/integration/unbind_spec.rb
64
+ - spec/integration/view_spec.rb
65
+ - spec/integration/zone_spec.rb
66
+ - spec/spec_helper.rb
67
+ - spec/unit/core_ext_spec.rb
68
+ - spec/unit/unbind_spec.rb
69
+ - spec/unit/view_spec.rb
70
+ - spec/unit/zone_spec.rb
71
+ - unbind.gemspec
72
+ homepage: https://github.com/krupenik/unbind
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.0.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: ISC BINDv9 config generator
96
+ test_files:
97
+ - spec/integration/unbind_spec.rb
98
+ - spec/integration/view_spec.rb
99
+ - spec/integration/zone_spec.rb
100
+ - spec/spec_helper.rb
101
+ - spec/unit/core_ext_spec.rb
102
+ - spec/unit/unbind_spec.rb
103
+ - spec/unit/view_spec.rb
104
+ - spec/unit/zone_spec.rb