bind9mgr 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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: []