bind9mgr 0.2.4

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.
data/.autotest ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/.gemtest ADDED
File without changes
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2011-09-09
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/bind9mgr
7
+ lib/bind9mgr.rb
8
+ lib/named_conf.rb
9
+ lib/zone.rb
10
+ lib/parser.rb
11
+ lib/resource_record.rb
12
+ spec/named_conf_spec.rb
13
+ spec/zone_spec.rb
14
+ spec/spec_helper.rb
15
+ spec/parser_spec.rb
16
+ spec/resource_record_spec.rb
data/README.txt ADDED
@@ -0,0 +1,66 @@
1
+ = bind9mgr
2
+
3
+ == DESCRIPTION:
4
+
5
+ This gem contains some classes to manage bind9 zone files
6
+
7
+ == FEATURES/PROBLEMS:
8
+
9
+ Please look into specs
10
+
11
+ == SYNOPSIS:
12
+
13
+ bc = Bind9mgr::NamedConf.new( '/etc/bind/named.conf.local',
14
+ :main_ns => 'ns1.example.com',
15
+ :secondary_ns => 'ns2.example.com',
16
+ :support_email => 'support@example.com',
17
+ :main_server_ip => '192.168.1.1'
18
+ )
19
+ bc.load_with_zones
20
+ bc.zones # => [ existing zones ... Bind9mgr::Zone ]
21
+ bc.zones.first.records # => [ records ... Bind9mgr::ResourceRecord ]
22
+
23
+
24
+ == REQUIREMENTS:
25
+
26
+ rspec >= 2.6.0
27
+ awesome_print
28
+ Tested with Ruby 1.9.2
29
+
30
+ == INSTALL:
31
+
32
+ gem install bind9mgr
33
+
34
+ == DEVELOPERS:
35
+
36
+ After checking out the source, run:
37
+
38
+ $ rake newb
39
+
40
+ This task will install any missing dependencies, run the tests/specs,
41
+ and generate the RDoc.
42
+
43
+ == LICENSE:
44
+
45
+ (The MIT License)
46
+
47
+ Copyright (c) 2011 FIX
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ 'Software'), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
63
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
64
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
65
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
66
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ # Hoe.plugin :compiler
7
+ # Hoe.plugin :gem_prelude_sucks
8
+ # Hoe.plugin :inline
9
+ # Hoe.plugin :racc
10
+ # Hoe.plugin :rubyforge
11
+
12
+ Hoe.spec 'bind9mgr' do
13
+ developer('Mikhail Barablin', 'mikhail@mad-box.ru')
14
+
15
+ # self.rubyforge_name = 'bind9mgrx' # if different than 'bind9mgr'
16
+ end
data/bin/bind9mgr ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ abort "you need to write me"
data/lib/bind9mgr.rb ADDED
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require File.join( File.dirname(__FILE__), 'named_conf' )
4
+ require File.join( File.dirname(__FILE__), 'zone' )
5
+ require File.join( File.dirname(__FILE__), 'resource_record' )
6
+ require File.join( File.dirname(__FILE__), 'parser' )
7
+
8
+ module Bind9mgr
9
+ VERSION = '0.2.4'
10
+
11
+ ZONES_BIND_SUBDIR = 'zones_db'
12
+
13
+ KLASSES = %w{IN CH}
14
+ ALLOWED_TYPES = %w{A CNAME MX TXT PTR NS SRV SOA}
15
+ end
data/lib/named_conf.rb ADDED
@@ -0,0 +1,124 @@
1
+ module Bind9mgr
2
+ # You can specify bind_location. If you do so then .add_zone method
3
+ # will generate zones with right filenames.
4
+ class NamedConf
5
+ # BIND_PATH = '/etc/bind'
6
+ # BIND_DB_PATH = BIND_PATH + '/master'
7
+
8
+ attr_accessor :file, :main_ns, :secondary_ns, :support_email, :main_server_ip, :bind_location
9
+ attr_reader :zones
10
+
11
+ def initialize( file = '' )
12
+ @file = file
13
+ @bind_location = File.dirname(file) if file.length > 1
14
+ load
15
+ end
16
+
17
+ def load
18
+ init_zones
19
+ parse File.read( File.join( @file )) if File.exists? file
20
+ end
21
+
22
+ def load_with_zones
23
+ init_zones
24
+ parse File.read( File.join( @file )) if File.exists? file
25
+ zones.each{ |z| z.load}
26
+ end
27
+
28
+
29
+ def parse content
30
+ content.scan(/(zone "(.*?)" \{.*?file\s+"(.*?)".*?\};\n)/m) do |zcontent, zone, file|
31
+ @zones.push Zone.new( zone, file,
32
+ :main_ns => @main_ns,
33
+ :secondary_ns => @secondary_ns,
34
+ :support_email => @support_email,
35
+ :main_server_ip => @main_server_ip )
36
+ end
37
+ @zones
38
+ end
39
+
40
+ def gen_conf_content
41
+ cont = '# File is under automatic control. Edit with caution.'
42
+ if @zones.size > 0
43
+ @zones.uniq.each do |zone|
44
+ cont << zone.gen_zone_entry << "\n"
45
+ end
46
+ end
47
+ cont
48
+ end
49
+
50
+ def write_conf_file
51
+ raise ArgumentError, "Conf file not specified" unless @file.kind_of? String
52
+ File.open( @file, 'w' ){|f| f.write( gen_conf_content )}
53
+ end
54
+
55
+ def write_zones
56
+ @zones.uniq.each do |z|
57
+ z.file ||= gen_zone_file_name;
58
+ zones_subdir = File.dirname(z.file)
59
+ Dir.mkdir( zones_subdir ) unless File.exists?( zones_subdir )
60
+ z.write_db_file
61
+ end if @zones.size > 0
62
+ end
63
+
64
+ def write_all
65
+ write_conf_file
66
+ write_zones
67
+ end
68
+
69
+ def add_zone( zone_or_name, file_name = nil )
70
+ if zone_or_name.kind_of?( Zone )
71
+ raise ArgumentError, "file_name should be nil if instance of Zone supplied" unless file_name.nil?
72
+ zone = zone_or_name
73
+ elsif zone_or_name.kind_of?( String )
74
+ raise ArgumentError, "Main ns not secified" unless @main_ns
75
+ # raise ArgumentError, "Secondary ns not secified" unless @secondary_ns
76
+ raise ArgumentError, "Support email not secified" unless @support_email
77
+ raise ArgumentError, "Main server ip not secified" unless @main_server_ip
78
+
79
+ zone = Zone.new( zone_or_name,
80
+ file_name || gen_zone_file_name(zone_or_name),
81
+ :main_ns => @main_ns,
82
+ :secondary_ns => @secondary_ns,
83
+ :support_email => @support_email,
84
+ :main_server_ip => @main_server_ip,
85
+ :mail_server_ip => @mail_server_ip)
86
+ else
87
+ raise( RuntimeError, "BindZone or String instance needed")
88
+ end
89
+
90
+ del_zone! zone.origin
91
+ @zones.push zone
92
+ end
93
+
94
+ # We should remove zone enties and delete db file immidiately.
95
+ def del_zone!( origin_or_name )
96
+ founded = @zones.select{ |z| z.origin == origin_or_name || z.name == origin_or_name }
97
+ founded.each do |z|
98
+ z.load
99
+ File.delete( z.file ) if File.exists? z.file
100
+ end
101
+ # TODO refactor!
102
+ if founded.count > 0
103
+ @zones.delete_if{ |z| z.origin == origin_or_name || z.name == origin_or_name }
104
+ end
105
+
106
+ # TODO unsafe code: other zone entries can be updated!
107
+ write_conf_file
108
+ end
109
+
110
+ private
111
+
112
+ def gen_zone_file_name( zone_name )
113
+ raise ArgumentError, "Bind location not specified" unless bind_location.kind_of?( String )
114
+ ext_zone_name = zone_name + '.db'
115
+
116
+ return ext_zone_name if bind_location.length < 1
117
+ return File.join( bind_location, ZONES_BIND_SUBDIR, ext_zone_name )
118
+ end
119
+
120
+ def init_zones
121
+ @zones = []
122
+ end
123
+ end
124
+ end
data/lib/parser.rb ADDED
@@ -0,0 +1,121 @@
1
+ module Bind9mgr
2
+ TYPES = %w{A CNAME TXT PTR NS SRV} # SOA, MX - are different
3
+ class Parser
4
+
5
+ attr_reader :state
6
+ attr_accessor :result # we can set appropriate Zone instance here
7
+
8
+ def initialize
9
+ @state = :start
10
+ @result = Zone.new
11
+
12
+ @STATE_RULES =
13
+ [ [:start, :origin, Proc.new{ |t| t == '$ORIGIN' }],
14
+ [:origin, :start, Proc.new{ |t| set_origin t }],
15
+ [:start, :ttl, Proc.new{ |t| t == '$TTL' }],
16
+ [:ttl, :start, Proc.new{ |t| set_ttl t }],
17
+ [:start, :type, Proc.new{ |t| TYPES.include?(t) ? add_rr(nil, nil, nil, t, nil) : false }],
18
+ [:start, :klass, Proc.new{ |t| KLASSES.include?(t) ? add_rr(nil, nil, t, nil, nil) : false }],
19
+ [:start, :rttl, Proc.new{ |t| t.match(/^\d+$/) ? add_rr(nil, t, nil, nil, nil) : false }],
20
+ [:start, :owner, Proc.new{ |t| add_rr(t, nil, nil, nil, nil) }],
21
+ [:owner, :rttl, Proc.new{ |t| t.match(/^\d+$/) ? update_last_rr(nil, t, nil, nil, nil) : false }],
22
+ [:owner, :klass, Proc.new{ |t| KLASSES.include?(t) ? update_last_rr(nil, nil, t, nil, nil) : false }],
23
+ [:owner, :type, Proc.new{ |t| TYPES.include?(t) ? update_last_rr(nil, nil, nil, t, nil) : false }],
24
+ [:rttl, :klass, Proc.new{ |t| KLASSES.include?(t) ? update_last_rr(nil, nil, t, nil, nil) : false }],
25
+ [:klass, :type, Proc.new{ |t| TYPES.include?(t) ? update_last_rr(nil, nil, nil, t, nil) : false }],
26
+ [:type, :start, Proc.new{ |t| update_last_rr(nil, nil, nil, nil, t) }],
27
+ [:klass, :soa, Proc.new{ |t| t == 'SOA' ? update_last_rr(nil, nil, nil, t, nil) : false }],
28
+ [:soa, :start, Proc.new{ |t| rdata = [t] + @tokens.shift(7)
29
+ raise RuntimeError, "Zone parsing error: parentices expected in SOA record.\n#{@content}" if (rdata[2] != '(') && (@tokens.first != ')')
30
+ rdata.delete_at(2)
31
+ @result.options[:support_email] = rdata[1]
32
+ @result.options[:serial] = rdata[2]
33
+ @result.options[:refresh] = rdata[3]
34
+ @result.options[:retry] = rdata[4]
35
+ @result.options[:expiry] = rdata[5]
36
+ @result.options[:default_ttl] = rdata[6]
37
+ update_last_rr(nil, nil, nil, nil, rdata)
38
+ @tokens.shift
39
+ }],
40
+ [:klass, :mx, Proc.new{ |t| t == 'MX' ? update_last_rr(nil, nil, nil, t, nil) : false }],
41
+ [:mx, :start, Proc.new{ |t| update_last_rr(nil, nil, nil, nil, [t] + [@tokens.shift]) }]
42
+ ]
43
+ end
44
+
45
+ def parse str
46
+ @content = str # for debugging
47
+ @tokens = tokenize( str )
48
+
49
+ cntr = 0
50
+ while @tokens.size > 0
51
+ token = @tokens.shift
52
+ # puts "state: #{@state}, token: #{token}"
53
+ possible_edges = @STATE_RULES.select{|arr|arr[0] == @state }
54
+ raise "no possible_edges. cur_state: #{@state}" if possible_edges.count < 1
55
+
56
+ flag = false
57
+ while ( possible_edges.count > 0 ) && flag == false
58
+ current_edge = possible_edges.shift
59
+ flag = current_edge[2].call(token)
60
+
61
+ # ( puts " succ: #{@state} -> #{current_edge[1]}"; @state = current_edge[1] ) if flag
62
+ # ( puts " fail: #{@state} -> #{current_edge[1]}" ) unless flag
63
+ @state = current_edge[1] if flag
64
+ end
65
+
66
+ raise "no successful rules found. cur_state: #{@state}, token: #{token}" unless flag
67
+ cntr += 1
68
+ end
69
+
70
+ get_options
71
+
72
+ cntr # returning performed rules count. just for fun
73
+ end
74
+
75
+ private
76
+
77
+ def tokenize str
78
+ str.gsub(/;.*$/, '').split(/\s/).select{|s|s.length > 0}
79
+ end
80
+
81
+ def get_options
82
+ # main server ip
83
+ main_a_rr = @result.records.find{ |r|(r.owner == '@' || r.owner == @result.origin || r.owner.nil?) && r.type == 'A' }
84
+ unless main_a_rr
85
+ puts "WARNING: main A rr not found. Can't get main server ip"
86
+ else
87
+ @result.options[:main_server_ip] = main_a_rr.rdata
88
+ end
89
+ # name servers
90
+ ns_rrs = @result.records.select{ |r| (r.owner == '@' || r.owner == @result.origin || r.owner.nil?) && r.type == 'NS' }
91
+ if ns_rrs.count < 1
92
+ puts "WARNING: NS rrs not found. Can't NS servers"
93
+ else
94
+ @result.options[:main_ns] = ns_rrs[0].rdata
95
+ @result.options[:secondary_ns] = ns_rrs[1].rdata if ns_rrs.count > 1
96
+ end
97
+ end
98
+
99
+ def set_origin val
100
+ @result.origin = val
101
+ end
102
+
103
+ def set_ttl val
104
+ @result.default_ttl = val
105
+ end
106
+
107
+ def add_rr owner, ttl, klass, type, rdata
108
+ @result.records ||= []
109
+ @result.records.push ResourceRecord.new( owner, ttl, klass, type, rdata )
110
+ end
111
+
112
+ def update_last_rr owner, ttl, klass, type, rdata
113
+ @result.records.last.owner = owner if owner
114
+ @result.records.last.ttl = ttl if ttl
115
+ @result.records.last.klass = klass if klass
116
+ @result.records.last.type = type if type
117
+ @result.records.last.rdata = rdata if rdata
118
+ return true
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,35 @@
1
+ module Bind9mgr
2
+ class ResourceRecord
3
+ attr_accessor :owner, :ttl, :klass, :type, :rdata
4
+
5
+ def initialize owner = nil, ttl = nil, klass = nil, type = nil, rdata = nil
6
+ @owner = owner
7
+ @ttl = ttl
8
+ @klass = klass
9
+ @type = type
10
+ @rdata = rdata
11
+ end
12
+
13
+ def gen_rr_string
14
+ raise ArgumentError, "RR Type not specified" unless @type
15
+ raise ArgumentError, "RR Rdata not specified" unless @rdata
16
+
17
+ raise( ArgumentError, "wrong owner: #{owner.inspect}" ) if owner == 'localhost'
18
+ raise( ArgumentError, "wrong class: #{klass.inspect}" ) if !klass.nil? && !KLASSES.include?( klass )
19
+ raise( ArgumentError, "wrong type: #{type.inspect}" ) unless ALLOWED_TYPES.include?( type )
20
+
21
+ if @type == 'SOA'
22
+ cont = ''
23
+ cont << "#{@owner}\t#{@ttl}\t#{@klass}\t#{@type}\t#{rdata[0]} #{rdata[1]} (\n"
24
+ cont << "\t#{Time.now.to_i} ; serial\n"
25
+ cont << "\t#{rdata[3]} ; refresh\n"
26
+ cont << "\t#{rdata[4]} ; retry\n"
27
+ cont << "\t#{rdata[5]} ; expire\n"
28
+ cont << "\t#{rdata[6]} ; minimum\n"
29
+ cont << ")\n"
30
+ else
31
+ "#{@owner}\t#{@ttl}\t#{@klass}\t#{@type}\t#{[@rdata].flatten.join(' ')}\n"
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/zone.rb ADDED
@@ -0,0 +1,194 @@
1
+ module Bind9mgr
2
+ class Zone
3
+ RRClasses = ['IN', 'CH']
4
+ RRTypes = [ 'A',
5
+ 'MX',
6
+ 'SRV',
7
+ 'CNAME',
8
+ 'SOA',
9
+ 'NS',
10
+ 'TXT',
11
+ 'PTR'
12
+ ]
13
+
14
+ attr_accessor :origin, :default_ttl
15
+ attr_accessor :file, :options
16
+ attr_reader :records
17
+
18
+ def initialize( zone_name = nil, zone_db_file = nil, options = { } )
19
+ @origin = zone_name
20
+ @file = zone_db_file
21
+ @options = options
22
+
23
+ @default_ttl ||= 86400
24
+ @options[:serial] ||= 109
25
+ @options[:refresh] ||= 3600
26
+ @options[:retry] ||= 3600
27
+ @options[:expiry] ||= 604800
28
+ @options[:default_ttl] ||= 86400
29
+
30
+ clear_records
31
+ end
32
+
33
+ def name
34
+ @origin.sub(/\.$/, '')
35
+ end
36
+
37
+ def name= str
38
+ @origin = str + '.'
39
+ end
40
+
41
+ # +rrs_array+ is a array like: [[owner, ttl, klass, type, rdata], ...]
42
+ def self.validete_rrs_uniqueness( rrs_array )
43
+ array = []
44
+ rrs_array.each do |owner, ttl, klass, type, rdata|
45
+ raise( ArgumentError, "owner, type and rdata have to be unique" ) if array.include? [owner,type,rdata]
46
+ array.push [owner,type,rdata]
47
+ end
48
+ end
49
+
50
+ def load
51
+ raise ArgumentError, "file not specified" unless @file
52
+
53
+ p = Parser.new
54
+ p.result = self
55
+ raise ArgumentError, "File: #{file} not found." unless File.exists?( @file )
56
+ p.parse File.read( @file )
57
+ end
58
+
59
+ # def parse content
60
+ # clear_records
61
+ # content.gsub!(/^\s+\n/, '')
62
+
63
+ # # find and remove SOA record with its comments
64
+ # soa = /([^\s]*?)\s+?(\d+?)?\s*?(#{RRClasses.join('|')})\s*?(SOA)\s+(.*?\).*?)\n/m
65
+ # arr = content.match(soa).to_a
66
+ # arr.shift
67
+ # @records['SOA'].push arr
68
+ # content.sub! soa, ''
69
+
70
+ # # remove comments and blank lines
71
+ # content.gsub!(/;.*$/, '')
72
+ # content.gsub!(/^\s+\n/, '')
73
+
74
+ # # other @Records
75
+ # rr = /^([^\s]+)\s+?(\d+?)?\s*?(#{RRClasses.join('|')})?\s*?(#{RRTypes.join('|')})\s+(.*?)$/
76
+ # content.lines.each do |l|
77
+ # if md = l.match(/\$TTL\s+(\d+)/)
78
+ # ( @default_ttl = md[1].to_i ) if md[1].to_i > 0
79
+ # elsif md = l.match(rr)
80
+ # tmp_a = md.to_a
81
+ # tmp_a.shift
82
+ # @records[tmp_a[3]].push tmp_a
83
+ # end
84
+ # end
85
+ # @records
86
+ # end
87
+
88
+ def gen_db_content
89
+ initialized?
90
+ raise ArgumentError, "default_ttl not secified" unless @default_ttl
91
+
92
+ add_default_rrs
93
+
94
+ cont = "; File is under automatic control. Edit with caution.\n"
95
+ cont << ";;; Zone #{@origin} ;;;" << "\n"
96
+ cont << "$ORIGIN #{@origin}" << "\n" if @origin
97
+ cont << "$TTL #{@default_ttl}" << "\n" if @default_ttl
98
+ cont << @records.map{ |r| r.gen_rr_string }.join
99
+
100
+ cont
101
+ end
102
+
103
+ def write_db_file
104
+ db_dir = File.dirname( @file )
105
+ raise( Errno::ENOENT, "No such dir: #{db_dir}" ) unless File.exists? db_dir
106
+ File.open( @file, 'w' ){|f| f.write( gen_db_content )}
107
+ end
108
+
109
+ def add_default_rrs
110
+ raise ArgumentError, "Main ns not specified" unless @options[:main_ns]
111
+ raise ArgumentError, "Main server ip not specified" unless @options[:main_server_ip]
112
+
113
+ ensure_soa_rr( default_soa )
114
+ ensure_rr( ResourceRecord.new('@', nil, 'IN', 'A', @options[:main_server_ip]) )
115
+ ensure_rr( ResourceRecord.new('@', nil, 'IN', 'NS', @options[:main_ns]) )
116
+ ensure_rr( ResourceRecord.new('@', nil, 'IN', 'NS', @options[:secondary_ns]) ) if @options[:secondary_ns]
117
+ # ensure_rr( ResourceRecord.new('@', nil, 'IN', 'MX', ['90', @options[:mail_server_ip]]) )
118
+ ensure_rr( ResourceRecord.new('www', nil, nil, 'CNAME', '@') )
119
+ end
120
+
121
+ def add_rr( owner, ttl, klass, type, rdata )
122
+ initialized?
123
+ @records.push ResourceRecord.new(owner, ttl, klass, type, rdata)
124
+ end
125
+
126
+ # removes all resourse record with specified owner and type
127
+ def remove_rr( owner, type )
128
+ raise( ArgumentError, "wrong owner" ) if owner.nil?
129
+ raise( ArgumentError, "wrong type" ) unless ALLOWED_TYPES.include? type
130
+
131
+ initialized?
132
+
133
+ @records.delete_if { |rr| (rr.owner == owner) && (rr.type == type) }
134
+ end
135
+
136
+ def gen_zone_entry
137
+ initialized?
138
+
139
+ cont = ''
140
+ cont << %Q|
141
+ zone "#{name}" {
142
+ type master;
143
+ file "#{@file}";
144
+ allow-update { none; };
145
+ allow-query { any; };
146
+ };
147
+ |
148
+ end
149
+
150
+ def clear_records
151
+ @records = []
152
+ end
153
+
154
+ private
155
+
156
+ def ensure_soa_rr record
157
+ cnt = @records.select{ |r| r.type == 'SOA' }.count
158
+ raise RuntimeError, "Multiple SOA detected. zone:#{@origin}" if cnt > 1
159
+ return false if cnt == 1
160
+ @records.unshift record
161
+ true
162
+ end
163
+
164
+ def ensure_rr record
165
+ max_rr_cnt = (record.type == 'NS' ? 2 : 1)
166
+ cnt = @records.select{ |rr| (rr.owner == record.owner) && (rr.type == record.type) }.count
167
+ raise RuntimeError, "Multiple rr with same owner+type detected. zone:#{@origin}" if cnt > max_rr_cnt
168
+ return false if cnt == max_rr_cnt
169
+ @records.push record
170
+ true
171
+ end
172
+
173
+ def default_soa
174
+ initialized?
175
+ raise ArgumentError, "Main ns not secified" unless @options[:main_ns]
176
+ raise ArgumentError, "Support email not secified" unless @options[:support_email]
177
+
178
+ ResourceRecord.new( '@', @options[:default_ttl], 'IN', 'SOA',
179
+ [ @origin,
180
+ @options[:support_email],
181
+ @options[:serial],
182
+ @options[:refresh],
183
+ @options[:retry],
184
+ @options[:expiry],
185
+ @options[:default_ttl]
186
+ ] )
187
+ end
188
+
189
+ def initialized?
190
+ raise( ArgumentError, "zone not initialized" ) if @origin.nil? || @file.nil?
191
+ end
192
+
193
+ end
194
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bind9mgr::NamedConf do
4
+ before do
5
+ @example_com_db_content = %q{$TTL 86400 ; 1 day
6
+ @ IN SOA testdomain.com. admin@testdomain.com. (
7
+ 1111083002 ; serial
8
+ 14400 ; refresh (4 h)
9
+ 3600 ; retry (1 h)
10
+ 2592000 ; expire (4w2d)
11
+ 600 ; minimum (10 minute)
12
+ )
13
+ @ IN NS ns.example.com.
14
+ sub1 IN A 192.168.1.2
15
+ sub2 IN A 192.168.1.3
16
+ alias1 IN CNAME ns
17
+ www CNAME @
18
+ }
19
+
20
+ @test_conf_content = %q{// test file
21
+ zone "cloud.ru" {
22
+ type master;
23
+ file "testdomain.com.db";
24
+ };
25
+
26
+ zone "1.168.192.in-addr.arpa" {
27
+ type master;
28
+ file "testdomain.com.db";
29
+ };
30
+ }
31
+
32
+ @test_db_content = %q{$ORIGIN testdomain.com.
33
+ $TTL 86400 ; 1 day
34
+ @ IN SOA testdomain.com. admin@testdomain.com. (
35
+ 2011083002 ; serial
36
+ 14400 ; refresh (4 h)
37
+ 3600 ; retry (1 h)
38
+ 2592000 ; expire (4w2d)
39
+ 600 ; minimum (10 minute)
40
+ )
41
+ IN NS ns.testdomain.com.
42
+ testdomain.com. IN A 192.168.1.1
43
+ sub1 IN A 192.168.1.2
44
+ sub2 IN A 192.168.1.3
45
+ alias1 IN CNAME ns
46
+ }
47
+
48
+ File.stub(:exists?).with(anything()).and_return(false)
49
+ File.stub(:exists?).with("testfile.conf").and_return(true)
50
+ File.stub(:exists?).with("testdomain.com.db").and_return(true)
51
+
52
+ File.stub(:read).with("testfile.conf").and_return(@test_conf_content)
53
+ File.stub(:read).with("testdomain.com.db").and_return(@test_db_content)
54
+
55
+ @nc = Bind9mgr::NamedConf.new
56
+ @nc.file = 'testfile.conf'
57
+ @nc.bind_location = ''
58
+ @nc.main_ns = 'ns1.example.com'
59
+ @nc.secondary_ns = 'ns2.example.com'
60
+ @nc.support_email = 'ns1.example.com'
61
+ @nc.main_server_ip = 'ns1.example.com'
62
+ @nc.load_with_zones
63
+ end
64
+
65
+ it "should be creatable" do
66
+ expect { Bind9mgr::NamedConf.new }.to_not raise_error
67
+ end
68
+
69
+ it "should fail to add_zone(some_string) unless bind_location filled" do
70
+ @nc.bind_location = nil
71
+ expect { @nc.add_zone('example.com') }.to raise_error(ArgumentError)
72
+ end
73
+
74
+ it "should fail to add_zone(some_string) unless main NS name filled" do
75
+ @nc.main_ns = nil
76
+ expect { @nc.add_zone('example.com') }.to raise_error(ArgumentError)
77
+ end
78
+
79
+ # Behaivor changed. Secondary ns absence causes warning only
80
+ # it "should fail to add_zone(some_string) unless secondary NS name filled" do
81
+ # @nc.secondary_ns = nil
82
+ # expect { @nc.add_zone('example.com') }.to raise_error(ArgumentError)
83
+ # end
84
+
85
+ it "should fail to add_zone(some_string) unless support email filled" do
86
+ @nc.support_email = nil
87
+ expect { @nc.add_zone('example.com') }.to raise_error(ArgumentError)
88
+ end
89
+
90
+ it "should fail to add_zone(some_string) unless main server ip filled" do
91
+ @nc.main_server_ip = nil
92
+ expect { @nc.add_zone('example.com') }.to raise_error(ArgumentError)
93
+ end
94
+
95
+ it "should fill @file if argument supplyed on instantiation" do
96
+ nc = Bind9mgr::NamedConf.new('testfile.conf')
97
+ nc.file.should eql 'testfile.conf'
98
+ end
99
+
100
+ it "should parse conf file on instantiation if supplyed and file exists" do
101
+ Bind9mgr::NamedConf.any_instance.should_receive(:parse).with(an_instance_of(String)).and_return(nil)
102
+ nc = Bind9mgr::NamedConf.new('testfile.conf')
103
+ end
104
+
105
+ it "should have zones after conf file parsing" do
106
+ nc = Bind9mgr::NamedConf.new('testfile.conf')
107
+ nc.zones.count.should == 2
108
+ nc.zones[0].should be_kind_of(Bind9mgr::Zone)
109
+ nc.zones[1].should be_kind_of(Bind9mgr::Zone)
110
+ end
111
+
112
+ it "should init zones before load" do
113
+ @nc.zones.count.should == 2 # as specified in "before"
114
+
115
+ @nc.file = "wrong.file.conf"
116
+ @nc.load
117
+ @nc.zones.should be_empty()
118
+ end
119
+
120
+ it "should have an empty array of zones on instantiation without conf file" do
121
+ nc = Bind9mgr::NamedConf.new
122
+ nc.zones.should be_empty()
123
+ end
124
+
125
+ it "should load zones data on 'load_with_zones'" do
126
+ @nc.load_with_zones
127
+ @nc.zones.first.records.count.should > 0
128
+ end
129
+
130
+ it "should remove old zone when zone with same name added" do
131
+ File.stub(:exists?).with("example.com.db").and_return(true) # lets think that there is no db file for this zone
132
+ File.stub(:read).with("example.com.db").and_return(@example_com_db_content)
133
+ File.stub(:delete).with("example.com.db").and_return(1)
134
+
135
+ @nc.add_zone( 'example.com' )
136
+ @nc.zones.last.add_rr( 'cname', nil, nil, 'CNAME', '@' )
137
+ @nc.zones.count.should == 3 # we specified 2 zones in "before"
138
+ @nc.add_zone( 'example.com' )
139
+ @nc.zones.last.records.count.should == 0
140
+ @nc.zones.last.add_default_rrs
141
+ @nc.zones.last.records.count.should == 5 # new zone should have SOA, A, CNAME(www) and 2*NS records
142
+ end
143
+
144
+ pending "should automatically generate file name for zone db if not supplied"
145
+ pending "should automatically make dir for zone db files"
146
+ pending "should have methods to edit SOA values"
147
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bind9mgr::Parser do
4
+ let(:test_zone) do
5
+ %q{$ORIGIN cloud.ru.
6
+ $TTL 86400 ; 1 day
7
+ @ IN SOA cloud.ru. root.cloud.ru. (
8
+ 2011083002 ; serial
9
+ 14400 ; refresh (4 h)
10
+ 3600 ; retry (1 h)
11
+ 2592000 ; expire (4w2d)
12
+ 600 ; minimum (10 minute)
13
+ )
14
+ IN NS ns.cloud.ru.
15
+ ns IN A 192.168.1.200
16
+ nfsstorage IN CNAME ns
17
+ mail IN MX 40 192.168.1.33
18
+ www CNAME @
19
+ cloud.ru. IN A 192.168.1.1
20
+ NS ns2.cloud.ru
21
+ manager IN A 192.168.1.20
22
+ director IN A 192.168.1.23
23
+ directorproxy IN A 192.168.1.24
24
+ oracle IN A 192.168.1.19
25
+ vcenter IN A 192.168.1.12
26
+ esx1 IN A 192.168.1.2
27
+ ; some comment
28
+ }
29
+ end
30
+
31
+ before do
32
+ p = Bind9mgr::Parser.new
33
+ p.parse test_zone
34
+ @result = p.result
35
+ end
36
+
37
+ it "should parse test data without errors" do
38
+ expect {
39
+ p = Bind9mgr::Parser.new
40
+ p.parse test_zone
41
+ }.not_to raise_error
42
+ end
43
+
44
+ it "should raise exception if there is no parentices in SOA definition" do
45
+ p = Bind9mgr::Parser.new
46
+ expect{
47
+ p.parse %q{
48
+ @ IN SOA cloud.ru. root.cloud.ru. (
49
+ 2011083002 ; serial
50
+ 14400 ; refresh (4 h)
51
+ 3600 ; retry (1 h)
52
+ 2592000 ; expire (4w2d)
53
+ 600 ; minimum (10 minute)
54
+
55
+ }
56
+ }.to raise_error
57
+ expect{
58
+ p.parse %q{
59
+ @ IN SOA cloud.ru. root.cloud.ru.
60
+ 2011083002 ; serial
61
+ 14400 ; refresh (4 h)
62
+ 3600 ; retry (1 h)
63
+ 2592000 ; expire (4w2d)
64
+ 600 ; minimum (10 minute)
65
+ )
66
+ }
67
+ }.to raise_error
68
+ end
69
+
70
+ it "should provide Zone with ResourceRecords" do
71
+ @result.should be_kind_of Bind9mgr::Zone
72
+ @result.records.count.should > 0
73
+ @result.records.first.should be_kind_of Bind9mgr::ResourceRecord # in hope that othes wil be the same
74
+ end
75
+
76
+ it "should have SOA record as first element of records" do
77
+ @result.records.first.type.should == 'SOA'
78
+ end
79
+
80
+ it "should have 7 elements in rdata of SOA record" do
81
+ @result.records.first.rdata.count.should == 7
82
+ end
83
+
84
+ it "should fill default_ttl" do
85
+ @result.default_ttl.should be
86
+ end
87
+
88
+ it "should parse CNAME records" do
89
+ @result.records[3].type.should == 'CNAME'
90
+ end
91
+
92
+ it "should parse A records" do
93
+ rr = @result.records[2]
94
+ rr.type.should == 'A'
95
+ rr.owner.should eql('ns')
96
+ rr.klass.should eql('IN')
97
+ rr.rdata.should eql('192.168.1.200')
98
+ end
99
+
100
+ it "should parse MX records(and mx priority!)" do
101
+ rr = @result.records[4]
102
+ rr.type.should == 'MX'
103
+ rr.owner.should eql('mail')
104
+ rr.klass.should eql('IN')
105
+ rr.rdata.should be_kind_of Array
106
+ rr.rdata.should eql(['40', '192.168.1.33'])
107
+ end
108
+
109
+ it "should parse records without ttl and class" do
110
+ rr = @result.records[5]
111
+ rr.type.should eql('CNAME')
112
+ rr.owner.should eql('www')
113
+ rr.klass.should eql(nil)
114
+ rr.rdata.should eql('@')
115
+ end
116
+
117
+ it "should parse records after records without ttl and class" do
118
+ rr = @result.records[6]
119
+ rr.owner.should eql('cloud.ru.')
120
+ rr.type.should eql('A')
121
+ rr.klass.should eql('IN')
122
+ rr.rdata.should eql('192.168.1.1')
123
+ end
124
+
125
+ pending "should parse PTR records (12 IN PTR something.com.)" do
126
+ end
127
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bind9mgr::ResourceRecord do
4
+ before do
5
+ @rr = Bind9mgr::ResourceRecord.new
6
+ @a_string = %q{example.com. IN A 192.168.1.1}
7
+ @cname_string = %q{cname IN CNAME @
8
+ }
9
+ @soa_string = %q{@ IN SOA cloud.ru. root.cloud.ru. (
10
+ 2011083002 ; serial
11
+ 14400 ; refresh (4 h)
12
+ 3600 ; retry (1 h)
13
+ 2592000 ; expire (4w2d)
14
+ 600 ; minimum (10 minute)
15
+ )
16
+ }
17
+ @mx_string = %q{example.com. IN MX 10 mail.example.com.}
18
+ end
19
+
20
+ it "should be instanceable" do
21
+ expect { Bind9mgr::ResourceRecord.new }.to_not raise_error
22
+ end
23
+
24
+ it "should have methods to fill parametrs" do
25
+ @rr.should respond_to( :owner, :ttl, :type, :klass, :rdata )
26
+ end
27
+
28
+ it "should have method to generate rr string" do
29
+ @rr.should respond_to( :gen_rr_string )
30
+ end
31
+
32
+ it "shoult have a list of allowed rr types" do
33
+ Bind9mgr::ALLOWED_TYPES.should be_kind_of(Array)
34
+ Bind9mgr::ALLOWED_TYPES.count.should > 0
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'ap'
3
+ require 'bind9mgr' # and any other gems you need
4
+
5
+ RSpec.configure do |config|
6
+ # some (optional) config here
7
+ end
data/spec/zone_spec.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bind9mgr::Zone do
4
+ before do
5
+ @test_db_content = %q{$ORIGIN testdomain.com.
6
+ $TTL 86400 ; 1 day
7
+ @ IN SOA testdomain.com. admin@testdomain.com. (
8
+ 2011083002 ; serial
9
+ 14400 ; refresh (4 h)
10
+ 3600 ; retry (1 h)
11
+ 2592000 ; expire (4w2d)
12
+ 600 ; minimum (10 minute)
13
+ )
14
+ IN NS ns.testdomain.com.
15
+ testdomain.com. IN A 192.168.1.1
16
+ sub1 IN A 192.168.1.2
17
+ sub2 IN A 192.168.1.3
18
+ alias1 IN CNAME ns
19
+ }
20
+
21
+ File.stub(:exists?).with(anything()).and_return(false)
22
+ File.stub(:exists?).with("testdomain.com.db").and_return(true)
23
+
24
+ File.stub(:read).with("testdomain.com.db").and_return(@test_db_content)
25
+
26
+ @zone = Bind9mgr::Zone.new
27
+ @zone.file = 'testdomain.com.db'
28
+ end
29
+
30
+ it "should be instanceable" do
31
+ expect{ Bind9mgr::Zone.new }.not_to raise_error
32
+ end
33
+
34
+ it "should fill itself with data on load method call" do
35
+ @zone.load
36
+ @zone.records.count.should > 0
37
+ end
38
+
39
+ pending "should fail to generate db file content unless mandatory options filled"
40
+ pending "should raise if wrong rr type specified"
41
+ pending "should not write repeating rrs"
42
+ it "should generate db file content" do
43
+ @zone.load
44
+ cont = @zone.gen_db_content
45
+ cont.should be_kind_of( String )
46
+ cont.match(/#{@zone.origin}/m).should be
47
+ end
48
+ pending "should generate zone entry content"
49
+
50
+ it "should add default rrs before generate db content" do
51
+ zone = Bind9mgr::Zone.new( 'example.com', 'example.com.db',
52
+ { :main_ns => '192.168.1.1',
53
+ :secondary_ns => '192.168.1.2',
54
+ :main_server_ip => '192.168.1.3',
55
+ :support_email => 'qwe@qwe.ru'
56
+ })
57
+ end
58
+
59
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bind9mgr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mikhail Barablin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-12 00:00:00.000000000 +04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ requirement: &9505680 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '2.12'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *9505680
26
+ description: This gem contains some classes to manage bind9 zone files
27
+ email:
28
+ - mikhail@mad-box.ru
29
+ executables:
30
+ - bind9mgr
31
+ extensions: []
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - .autotest
38
+ - History.txt
39
+ - Manifest.txt
40
+ - README.txt
41
+ - Rakefile
42
+ - bin/bind9mgr
43
+ - lib/bind9mgr.rb
44
+ - lib/named_conf.rb
45
+ - lib/zone.rb
46
+ - lib/parser.rb
47
+ - lib/resource_record.rb
48
+ - spec/named_conf_spec.rb
49
+ - spec/zone_spec.rb
50
+ - spec/spec_helper.rb
51
+ - spec/parser_spec.rb
52
+ - spec/resource_record_spec.rb
53
+ - .gemtest
54
+ has_rdoc: true
55
+ homepage:
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --main
60
+ - README.txt
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project: bind9mgr
77
+ rubygems_version: 1.6.2
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: This gem contains some classes to manage bind9 zone files
81
+ test_files: []