unbind 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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