zonefile 1.00
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/zonefile/zonefile.rb +263 -0
- data/lib/zonefile.rb +1 -0
- data/tests/test-zone.db +32 -0
- data/tests/zonefile.rb +141 -0
- metadata +57 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
#
|
2
|
+
# = Ruby Zonefile - Parse and manipulate DNS Zone Files.
|
3
|
+
#
|
4
|
+
# == Description
|
5
|
+
# This class can read, manipulate and create DNS zone files. It supports A, AAAA, MX, NS, SOA,
|
6
|
+
# TXT, CNAME and SRV records. The data can be accessed by the instance method of the same
|
7
|
+
# name. All except SOA return an array of hashes containing the named data. SOA directly returns the
|
8
|
+
# hash since there can only be one SOA information.
|
9
|
+
#
|
10
|
+
# The following hash keys are returned per record type:
|
11
|
+
#
|
12
|
+
# * SOA
|
13
|
+
# - :ttl, :primary, :email, :serial, :refresh, :retry, :expire, :minimumTTL
|
14
|
+
# * A
|
15
|
+
# - :name, :ttl, :class, :host
|
16
|
+
# * MX
|
17
|
+
# - :name, :ttl, :class, :pri, :host
|
18
|
+
# * NS
|
19
|
+
# - :name, :ttl, :class, :host
|
20
|
+
# * CNAME
|
21
|
+
# - :name, :ttl, :class, :host
|
22
|
+
# * TXT
|
23
|
+
# - :name, :ttl, :class, :text
|
24
|
+
# * A4 (AAAA)
|
25
|
+
# - :name, :ttl, :class, :host
|
26
|
+
# * SRV
|
27
|
+
# - :name, :ttl, :class, :pri, :weight, :port, :host
|
28
|
+
#
|
29
|
+
# == Examples
|
30
|
+
#
|
31
|
+
# === Read a Zonefile
|
32
|
+
#
|
33
|
+
# zf = Zonefile.from_file('/path/to/zonefile.db')
|
34
|
+
#
|
35
|
+
# # Display MX-Records
|
36
|
+
# zf.mx.each do |mx_record|
|
37
|
+
# puts "Mail Exchagne with priority: #{mx_record[:pri]} --> #{mx_record[:host]}"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# # Show SOA TTL
|
41
|
+
# puts "Record Time To Live: #{zf.soa[:ttl]}"
|
42
|
+
#
|
43
|
+
# # Show A-Records
|
44
|
+
# zf.a.each do |a_record|
|
45
|
+
# puts "#{a_record[:name]} --> #{a_record[:host]}"
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
#
|
49
|
+
# ==== Manipulate a Zonefile
|
50
|
+
#
|
51
|
+
# zf = Zonefile.from_file('/path/to/zonefile.db')
|
52
|
+
#
|
53
|
+
# # Change TTL and add an A-Record
|
54
|
+
#
|
55
|
+
# zf.soa[:ttl] = '123123' # Change the SOA ttl
|
56
|
+
# zf.a << { :class => 'IN', :name => 'www', :host => '192.168.100.1', :ttl => 3600 } # add A-Record
|
57
|
+
#
|
58
|
+
# # Increase Serial Number
|
59
|
+
# zf.new_serial
|
60
|
+
#
|
61
|
+
# # Print new zonefile
|
62
|
+
# puts "New Zonefile: \n#{zf.output}"
|
63
|
+
#
|
64
|
+
# == Author
|
65
|
+
#
|
66
|
+
# Martin Boese, based on Simon Flack Perl library DNS::ZoneParse
|
67
|
+
#
|
68
|
+
|
69
|
+
class Zonefile
|
70
|
+
|
71
|
+
RECORDS = %w{ mx a a4 ns cname txt ptr srv soa }
|
72
|
+
attr :records
|
73
|
+
attr :soa
|
74
|
+
attr :data
|
75
|
+
|
76
|
+
def method_missing(m)
|
77
|
+
return super unless RECORDS.include?(m.to_s)
|
78
|
+
@records[m]
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Compact a zonefile content - removes empty lines, comments,
|
83
|
+
# converts tabs into spaces etc...
|
84
|
+
def self.simplify(zf)
|
85
|
+
# concatenate everything split over multiple lines in parentheses - remove ;-comments in block
|
86
|
+
zf = zf.gsub(/(\([^\)]*?\))/) { |m| m.split(/\n/).map { |l| l.gsub(/\;.*$/, '') }.join("\n").gsub(/[\r\n]/, '') }
|
87
|
+
|
88
|
+
zf.split(/\n/).map do |line|
|
89
|
+
r = line.gsub(/\t/, ' ')
|
90
|
+
r = r.gsub(/\s+/, ' ')
|
91
|
+
r = r.gsub(/\;.*$/, '')
|
92
|
+
end.delete_if { |line| line.empty? || line[0].chr == ';'}.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# create a new zonefile object by passing the content of the zonefile
|
97
|
+
def initialize(zonefile = '', file_name= nil, origin= nil)
|
98
|
+
@data = zonefile
|
99
|
+
@filename = file_name
|
100
|
+
@origin = origin
|
101
|
+
|
102
|
+
@records = {}
|
103
|
+
@soa = {}
|
104
|
+
RECORDS.each { |r| @records[r.intern] = [] }
|
105
|
+
parse
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create a new object by reading the content of a file
|
109
|
+
def self.from_file(file_name, origin = nil)
|
110
|
+
Zonefile.new(File.read(file_name), file_name.split('/').last, origin)
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_record(type, data= {})
|
114
|
+
@records[type.downcase.intern] << data
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generates a new serial number in the format of YYYYMMDDII if possible
|
118
|
+
def new_serial
|
119
|
+
base = "%04d%02d%02d" % [Time.now.year, Time.now.month, Time.now.day ]
|
120
|
+
|
121
|
+
if ((self.soa[:serial].to_i / 100) > base.to_i) then
|
122
|
+
ns = self.soa[:serial].to_i + 1
|
123
|
+
self.soa[:serial] = ns.to_s
|
124
|
+
return ns.to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
ii = 0
|
128
|
+
while (("#{base}%02d" % ii).to_i <= self.soa[:serial].to_i) do
|
129
|
+
ii += 1
|
130
|
+
end
|
131
|
+
self.soa[:serial] = "#{base}%02d" % ii
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_line(line)
|
135
|
+
valid_name = /[\@a-z_\-\.0-9\*]+/i
|
136
|
+
valid_ip6 = /[\@a-z_\-\.0-9\*:]+/i
|
137
|
+
rr_class = /\b(?:IN|HS|CH)\b/i
|
138
|
+
rr_type = /\b(?:NS|A|CNAME)\b/i
|
139
|
+
rr_ttl = /(?:\d+[wdhms]?)+/i
|
140
|
+
ttl_cls = Regexp.new("(?:(#{rr_ttl})\s)?(?:(#{rr_class})\s)?")
|
141
|
+
|
142
|
+
data = {}
|
143
|
+
if line =~ /^(#{valid_name})? \s*
|
144
|
+
#{ttl_cls}
|
145
|
+
(#{rr_type}) \s
|
146
|
+
(#{valid_name})
|
147
|
+
/ix then
|
148
|
+
(name, ttl, dclass, type, host) = [$1, $2, $3, $4, $5]
|
149
|
+
add_record($4, :name => $1, :ttl => $2, :class => $3, :host => $5)
|
150
|
+
elsif line=~/^(#{valid_name})? \s*
|
151
|
+
#{ttl_cls}
|
152
|
+
AAAA \s
|
153
|
+
(#{valid_ip6})
|
154
|
+
/x then
|
155
|
+
add_record('a4', :name => $1, :ttl => $2, :class => $3, :host => $4)
|
156
|
+
elsif line=~/^(#{valid_name})? \s*
|
157
|
+
#{ttl_cls}
|
158
|
+
MX \s
|
159
|
+
(\d+) \s
|
160
|
+
(#{valid_name})
|
161
|
+
/ix then
|
162
|
+
add_record('mx', :name => $1, :ttl => $2, :class => $3, :pri => $4.to_i, :host => $5)
|
163
|
+
elsif line=~/^(#{valid_name})? \s*
|
164
|
+
#{ttl_cls}
|
165
|
+
SRV \s
|
166
|
+
(\d+) \s
|
167
|
+
(\d+) \s
|
168
|
+
(\d+) \s
|
169
|
+
(#{valid_name})
|
170
|
+
/ix
|
171
|
+
add_record('srv', :name => $1, :ttl => $2, :class => $3, :pri => $4, :weight => $5,
|
172
|
+
:port => $6, :host => $7)
|
173
|
+
elsif line=~/^(#{valid_name}) \s+
|
174
|
+
#{ttl_cls}
|
175
|
+
SOA \s+
|
176
|
+
(#{valid_name}) \s+
|
177
|
+
(#{valid_name}) \s*
|
178
|
+
\(?\s*
|
179
|
+
(#{rr_ttl}) \s+
|
180
|
+
(#{rr_ttl}) \s+
|
181
|
+
(#{rr_ttl}) \s+
|
182
|
+
(#{rr_ttl}) \s+
|
183
|
+
(#{rr_ttl}) \s*
|
184
|
+
\)?
|
185
|
+
/ix
|
186
|
+
ttl = @soa[:ttl] || $2 || ''
|
187
|
+
@soa[:origin] = $1
|
188
|
+
@soa[:ttl] = ttl
|
189
|
+
@soa[:primary] = $4
|
190
|
+
@soa[:email] = $5
|
191
|
+
@soa[:serial] = $6
|
192
|
+
@soa[:refresh] = $7
|
193
|
+
@soa[:retry] = $8
|
194
|
+
@soa[:expire] = $9
|
195
|
+
@soa[:minimumTTL] = $10
|
196
|
+
|
197
|
+
elsif line=~ /^(#{valid_name})? \s*
|
198
|
+
#{ttl_cls}
|
199
|
+
PTR \s+
|
200
|
+
(#{valid_name})
|
201
|
+
/ix
|
202
|
+
add_record('ptr', :name => $1, :class => $3, :ttl => $2, :host => $4)
|
203
|
+
elsif line =~ /(#{valid_name})? \s #{ttl_cls} TXT \s \"([^\"]*)\"/ix
|
204
|
+
add_record('txt', :name => $1, :ttl => $2, :class => $3, :text => $4)
|
205
|
+
elsif line =~ /\$TTL\s+(#{rr_ttl})/i
|
206
|
+
@soa[:ttl] = $1
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse
|
211
|
+
Zonefile.simplify(@data).each_line do |line|
|
212
|
+
parse_line(line)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# Build a new nicely formatted Zonefile
|
218
|
+
#
|
219
|
+
def output
|
220
|
+
out =<<-ENDH
|
221
|
+
;
|
222
|
+
; Database file #{@filename || 'unknown'} for #{@origin || 'unknown'} zone.
|
223
|
+
; Zone version: #{self.soa[:serial]}
|
224
|
+
;
|
225
|
+
#{self.soa[:ttl] ? "$TTL #{self.soa[:ttl]}" : ''}
|
226
|
+
#{self.soa[:origin]} #{self.soa[:ttl]} IN SOA #{self.soa[:primary]} #{self.soa[:email]} (
|
227
|
+
#{self.soa[:serial]} ; serial number
|
228
|
+
#{self.soa[:refresh]} ; refresh
|
229
|
+
#{self.soa[:retry]} ; retry
|
230
|
+
#{self.soa[:expire]} ; expire
|
231
|
+
#{self.soa[:minimumTTL]} ; minimum TTL
|
232
|
+
)
|
233
|
+
; Zone NS Records
|
234
|
+
ENDH
|
235
|
+
self.ns.each do |ns|
|
236
|
+
out << "#{ns[:name]} #{ns[:ttl]} #{ns[:class]} NS #{ns[:host]}\n"
|
237
|
+
end
|
238
|
+
out << "\n; Zone MX Records\n" unless self.mx.empty?
|
239
|
+
self.mx.each do |mx|
|
240
|
+
out << "#{mx[:name]} #{mx[:ttl]} #{mx[:class]} MX #{mx[:pri]} #{mx[:host]}\n"
|
241
|
+
end
|
242
|
+
|
243
|
+
self.a.each do |a|
|
244
|
+
out << "#{a[:name]} #{a[:ttl]} #{a[:class]} A #{a[:host]}\n"
|
245
|
+
end
|
246
|
+
self.cname.each do |cn|
|
247
|
+
out << "#{cn[:name]} #{cn[:ttl]} #{cn[:class]} CNAME #{cn[:host]}\n"
|
248
|
+
end
|
249
|
+
self.a4.each do |a4|
|
250
|
+
out << "#{a4[:name]} #{a4[:ttl]} #{a4[:class]} AAAA #{a4[:host]}\n"
|
251
|
+
end
|
252
|
+
self.txt.each do |tx|
|
253
|
+
out << "#{tx[:name]} #{tx[:ttl]} #{tx[:class]} TXT \"#{tx[:text]}\"\n"
|
254
|
+
end
|
255
|
+
self.srv.each do |srv|
|
256
|
+
out << "#{srv[:name]} #{srv[:ttl]} #{srv[:class]} SRV #{srv[:pri]} #{srv[:weight]} #{srv[:port]} #{srv[:host]}\n"
|
257
|
+
end
|
258
|
+
|
259
|
+
out
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
data/lib/zonefile.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'zonefile/zonefile'
|
data/tests/test-zone.db
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
; Database file dns-zoneparse-test.net.dns for dns-zoneparse-test.net zone.
|
2
|
+
; Zone version: 2000100501
|
3
|
+
$TTL 1H
|
4
|
+
@ 3600 IN SOA ns0.dns-zoneparse-test.net. support.dns-zoneparse-test.net. (
|
5
|
+
2000100501 ; serial number
|
6
|
+
10800 ; refresh
|
7
|
+
3600 ; retry
|
8
|
+
691200 ; expire
|
9
|
+
86400 ) ; minimum TTL
|
10
|
+
|
11
|
+
43200 IN NS ns0.dns-zoneparse-test.net. ; ( A multi line
|
12
|
+
comment )
|
13
|
+
@ IN NS ns1.dns-zoneparse-test.net.
|
14
|
+
|
15
|
+
@ IN A 127.0.0.1
|
16
|
+
@ IN MX 10 mail
|
17
|
+
ftp IN CNAME www
|
18
|
+
localhost IN A 127.0.0.1
|
19
|
+
mail IN A 127.0.0.1
|
20
|
+
www IN A 127.0.0.1
|
21
|
+
in a 10.0.0.2
|
22
|
+
43200 IN A 10.0.0.3
|
23
|
+
IN MX 10 10.0.0.4
|
24
|
+
A 10.0.0.5
|
25
|
+
TXT "web server"
|
26
|
+
foo IN A 10.0.0.6
|
27
|
+
mini A 10.0.0.7
|
28
|
+
icarus IN AAAA fe80::0260:83ff:fe7c:3a2a
|
29
|
+
soup IN TXT "This is a text message"
|
30
|
+
txta TXT "This is another text message"
|
31
|
+
_sip._tcp.example.com. 86400 IN SRV 0 5 5060 sipserver.example.com.
|
32
|
+
|
data/tests/zonefile.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
|
4
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
5
|
+
|
6
|
+
require 'zonefile'
|
7
|
+
|
8
|
+
$zonefile = ARGV[0] || 'test-zone.db'
|
9
|
+
|
10
|
+
class TC_Zonefile < Test::Unit::TestCase
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@zf = Zonefile.from_file(File.dirname(__FILE__) + '/'+$zonefile, 'test-origin')
|
14
|
+
end
|
15
|
+
|
16
|
+
def swap # generate output and re-read @zf from it
|
17
|
+
@zf = Zonefile.new(@zf.output, 'test-origin')
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_soa
|
21
|
+
assert_equal '86400', @zf.soa[:minimumTTL]
|
22
|
+
assert_equal '691200', @zf.soa[:expire]
|
23
|
+
assert_equal '3600', @zf.soa[:retry]
|
24
|
+
assert_equal '10800', @zf.soa[:refresh]
|
25
|
+
assert_equal '2000100501', @zf.soa[:serial]
|
26
|
+
assert_equal 'support.dns-zoneparse-test.net.', @zf.soa[:email]
|
27
|
+
assert_equal 'ns0.dns-zoneparse-test.net.', @zf.soa[:primary]
|
28
|
+
|
29
|
+
begin
|
30
|
+
@swap_soa = true
|
31
|
+
swap
|
32
|
+
test_soa
|
33
|
+
end unless @swap_soa
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_a
|
37
|
+
assert_equal 9, @zf.a.size
|
38
|
+
assert_equal 'mini', @zf.a.last[:name]
|
39
|
+
assert_equal '10.0.0.7', @zf.a.last[:host]
|
40
|
+
assert_equal '127.0.0.1', @zf.a.first[:host]
|
41
|
+
|
42
|
+
a = @zf.a.find { |a| a[:host] == '10.0.0.3'}
|
43
|
+
assert_equal '43200', a[:ttl]
|
44
|
+
assert_equal '', a[:name].to_s
|
45
|
+
|
46
|
+
begin
|
47
|
+
@swap_a = true
|
48
|
+
swap
|
49
|
+
test_a
|
50
|
+
end unless @swap_a
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_mx
|
54
|
+
assert_equal 2, @zf.mx.size
|
55
|
+
assert_equal 10, @zf.mx.first[:pri]
|
56
|
+
begin
|
57
|
+
@swap_mx = true
|
58
|
+
swap
|
59
|
+
test_mx
|
60
|
+
end unless @swap_mx
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_cname
|
64
|
+
assert !!@zf.cname.find { |e| e[:host] == 'www' }
|
65
|
+
begin
|
66
|
+
@swap_cname = true
|
67
|
+
swap
|
68
|
+
test_cname
|
69
|
+
end unless @swap_cname
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_ns
|
73
|
+
assert_equal 'ns0.dns-zoneparse-test.net.', @zf.ns[0][:host]
|
74
|
+
assert_equal 'ns1.dns-zoneparse-test.net.', @zf.ns[1][:host]
|
75
|
+
begin
|
76
|
+
@swap_ns = true
|
77
|
+
swap
|
78
|
+
test_ns
|
79
|
+
end unless @swap_ns
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_txt
|
83
|
+
assert_equal 'web server', @zf.txt[0][:text]
|
84
|
+
assert_equal 'IN', @zf.txt[1][:class]
|
85
|
+
assert_equal 'soup', @zf.txt[1][:name]
|
86
|
+
assert_equal 'txta', @zf.txt[2][:name]
|
87
|
+
assert_equal 3, @zf.txt.size
|
88
|
+
begin
|
89
|
+
@swap_txt = true
|
90
|
+
swap
|
91
|
+
test_txt
|
92
|
+
end unless @swap_txt
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_a4
|
96
|
+
assert_equal 'icarus', @zf.a4[0][:name]
|
97
|
+
assert_equal 'IN', @zf.a4[0][:class]
|
98
|
+
assert_equal 1, @zf.a4.size
|
99
|
+
assert_equal 'fe80::0260:83ff:fe7c:3a2a', @zf.a4[0][:host]
|
100
|
+
begin
|
101
|
+
@swap_a4 = true
|
102
|
+
swap
|
103
|
+
test_a4
|
104
|
+
end unless @swap_a4
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_srv
|
108
|
+
assert_equal '_sip._tcp.example.com.', @zf.srv[0][:name]
|
109
|
+
assert_equal '86400', @zf.srv[0][:ttl]
|
110
|
+
assert_equal '0', @zf.srv[0][:pri]
|
111
|
+
assert_equal '5', @zf.srv[0][:weight]
|
112
|
+
assert_equal '5060', @zf.srv[0][:port]
|
113
|
+
assert_equal 'sipserver.example.com.', @zf.srv[0][:host]
|
114
|
+
begin
|
115
|
+
@swap_srv = true
|
116
|
+
swap
|
117
|
+
test_srv
|
118
|
+
end unless @swap_srv
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_serial_generator
|
122
|
+
old = @zf.soa[:serial]
|
123
|
+
new = @zf.new_serial
|
124
|
+
assert new.to_i > old.to_i
|
125
|
+
newer = @zf.new_serial
|
126
|
+
assert newer.to_i - 1, new
|
127
|
+
|
128
|
+
@zf.soa[:serial] = '9999889901'
|
129
|
+
@zf.new_serial
|
130
|
+
assert_equal '9999889902', @zf.soa[:serial]
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def test_output
|
136
|
+
# puts @zf.data
|
137
|
+
# puts '==================='
|
138
|
+
# puts @zf.output
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zonefile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.00"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin Boese
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-04 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: martin@internet.ao
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/zonefile
|
26
|
+
- lib/zonefile/zonefile.rb
|
27
|
+
- lib/zonefile.rb
|
28
|
+
- tests/test-zone.db
|
29
|
+
- tests/zonefile.rb
|
30
|
+
has_rdoc: true
|
31
|
+
homepage: http://zonefile.rubyforge.org/
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: "0"
|
42
|
+
version:
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
rubyforge_project: zonefile
|
52
|
+
rubygems_version: 1.2.0
|
53
|
+
signing_key:
|
54
|
+
specification_version: 2
|
55
|
+
summary: BIND 8/9 Zonefile Reader and Writer
|
56
|
+
test_files: []
|
57
|
+
|