redzone 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,279 @@
1
+ require 'redzone/record'
2
+ require 'redzone/soa'
3
+ require 'redzone/machine'
4
+ require 'redzone/mail_exchange'
5
+ require 'redzone/name_server'
6
+ require 'redzone/arpa'
7
+
8
+ module RedZone
9
+ # Represents a zone configuraton for a domain.
10
+ #
11
+ # Takes a zone configuration in the form of a hash.
12
+ # this is usually read from a yaml file. The schema
13
+ # for the yaml file can be found in the resources
14
+ #
15
+ class Zone
16
+ # Returns the domain name of the zone
17
+ # @return [String]
18
+ attr_reader :name
19
+
20
+ # Return the a list of machines
21
+ # @return [Array<Machine>]
22
+ attr_reader :machines
23
+
24
+ # Return the SOA record
25
+ # @return [SOA]
26
+ attr_reader :soa
27
+
28
+ # Returns the default machine for the domain when no subdomain is supplied
29
+ # @return [Machine]
30
+ attr_reader :default
31
+
32
+ # Returns a list of name servers that service the zone
33
+ # @return [Array<Machine>]
34
+ attr_reader :nameservers
35
+
36
+ # Returns a list of mail servers
37
+ # @return [Array<Machine>]
38
+ attr_reader :mailservers
39
+
40
+ # Returns a list of alternate names for machines.
41
+ # These are additional A/AAAA records to the same
42
+ # IP Address
43
+ # @return [Array<Machine>]
44
+ attr_reader :altnames
45
+
46
+ # Returns a list of domain-name aliases
47
+ # @return [Array<Record>]
48
+ attr_reader :cnames
49
+
50
+ # Returns the wildcard entry - the catch-all machine for dns queries
51
+ # not matching any other subdomain entry.
52
+ # @return [Machine]
53
+ attr_reader :wildcard
54
+
55
+ # Returns a list of additional zone records
56
+ # @return [Array<Record>]
57
+ attr_reader :records
58
+
59
+ # Create a new Zone config
60
+ # @param [String] name Domain name (eg: example.com)
61
+ # @param [Hash] config Zone configuration
62
+ def initialize(name,config)
63
+ @name = name
64
+ @config = config
65
+ generate_machines()
66
+ generate_aliases()
67
+ generate_nameservers()
68
+ generate_mailservers()
69
+ generate_cnames()
70
+ generate_records()
71
+ @soa = generate_soa(@name)
72
+ d = get_machine('default')
73
+ wc = get_machine('wildcard')
74
+ if d
75
+ @default = d.alias("@")
76
+ end
77
+ if wc
78
+ @wildcard = wc.alias("*")
79
+ end
80
+ end
81
+
82
+ # Writes a given set of Records to the target IO
83
+ # @param [IO] io output IO Stream
84
+ # @param [Array<Record>] records
85
+ def write_records(io,records)
86
+ unless records.nil? or records.empty?
87
+ records.each do |r|
88
+ io << r
89
+ end
90
+ end
91
+ end
92
+
93
+ # Writes the list of machines to the target IO
94
+ # @param [IO] io output IO Stream
95
+ # @param [Array<#records>] machines machines
96
+ # @param [String] comment (nil) Optional comment to be prefixed to the record section
97
+ def write_machines(io,machines,comment=nil)
98
+ unless machines.nil? or machines.empty?
99
+ io << "; #{comment}\n" unless comment.nil?
100
+ machines.each do |m|
101
+ write_records(io,m.records)
102
+ end
103
+ io << "\n"
104
+ end
105
+ end
106
+
107
+ # Writes the entre zonefile to the target IO
108
+ # @param [IO] io Target IO stream
109
+ def write(io)
110
+ io << soa
111
+ unless default.nil?
112
+ write_machines(io,[default],"Default Machine")
113
+ end
114
+ write_machines(io,nameservers,"Name Servers")
115
+ write_machines(io,mailservers,"Mail Servers")
116
+ write_machines(io,machines,"Primary Machine names")
117
+ write_machines(io,altnames,"Alternate Machine Names")
118
+ unless wildcard.nil?
119
+ write_machines(io,[wildcard],"Wildcard Machine")
120
+ end
121
+ unless cnames.empty?
122
+ io << "; Canonical Names\n"
123
+ write_records(io,cnames)
124
+ io << "\n"
125
+ end
126
+
127
+ unless records.nil? or records.empty?
128
+ io << "; Extra Records\n"
129
+ write_records(io,records)
130
+ io << "\n"
131
+ end
132
+ end
133
+
134
+ # Generates a list of Arpas for this zone
135
+ # @return [Array<Arpa>]
136
+ def generate_arpa_list
137
+ arpas = []
138
+ if @config.has_key?('arpa')
139
+ @config['arpa'].each do |cfg|
140
+ opts = symbolize(cfg)
141
+ soa = generate_soa(opts[:name])
142
+ opts[:soa] = soa
143
+ an = Arpa.new(opts)
144
+ arpas << an
145
+ @nameservers.each do |ns|
146
+ an.add_record(Record.new(:name => '@', :type => 'NS', :data => ns.name + ".#{@name}." ))
147
+ end
148
+ @machines.each do |machine|
149
+ an.add(machine,@name)
150
+ end
151
+ end
152
+ end
153
+ arpas
154
+ end
155
+
156
+ private
157
+ def sorted_each(hash,&block)
158
+ keys = hash.keys.sort
159
+ keys.each do |key|
160
+ block.call(key,hash[key])
161
+ end
162
+ end
163
+ def symbolize(hash)
164
+ hash.inject({}) do |memo,(k,v)|
165
+ memo[k.to_sym] = v
166
+ memo
167
+ end
168
+ end
169
+ def generate_soa(name)
170
+ opts = symbolize(@config['lifetimes'])
171
+ opts[:hostmaster] = @config['hostmaster'] if @config.has_key?('hostmaster')
172
+ opts[:domain] = name
173
+ opts[:ns] = @nameservers[0].name + ".#{@name}." unless @nameservers.empty?
174
+ SOA.new(opts)
175
+ end
176
+ def generate_machines
177
+ @machines = []
178
+ @allnames = []
179
+ @machines_by_name = {}
180
+ sorted_each(@config['machines']) do |n,c|
181
+ #@config['machines'].each do |n,c|
182
+ m = Machine.new(n,symbolize(c))
183
+ @allnames << n
184
+ @machines << m
185
+ @machines_by_name[n] = m
186
+ end
187
+ end
188
+ def get_machine(name)
189
+ if @machines_by_name.has_key?(name)
190
+ @machines_by_name[name]
191
+ else
192
+ nil
193
+ end
194
+ end
195
+ def generate_aliases
196
+ machines = []
197
+ if @config.has_key?('aliases')
198
+ sorted_each(@config['aliases']) do |name,ref|
199
+ #@config['aliases'].each do |name,ref|
200
+ m = get_machine(ref)
201
+ if not m.nil?
202
+ machines << m.alias(name)
203
+ @allnames << name
204
+ else
205
+ puts "ERROR: No such machine named #{ref}"
206
+ end
207
+ end
208
+ end
209
+ @altnames = machines
210
+ end
211
+ def add_alias(name,machine)
212
+ unless @allnames.include?(name)
213
+ @altnames << machine.alias(name)
214
+ @allnames << name
215
+ end
216
+ end
217
+ def generate_mailservers
218
+ machines = []
219
+ if @config.has_key?('mailservers')
220
+ sorted_each(@config['mailservers']) do |name,mx|
221
+ #@config['mailservers'].each do |name,mx|
222
+ ref = mx['machine']
223
+ m = get_machine(ref)
224
+ if not m.nil?
225
+ priority = mx['priority'] || 10
226
+ machines << MailExchange.new(name,m,priority)
227
+ add_alias(name,m)
228
+ else
229
+ puts "ERROR: No such machine named #{ref}"
230
+ end
231
+ end
232
+ end
233
+ @mailservers = machines
234
+ end
235
+ def generate_nameservers
236
+ machines = []
237
+ if @config.has_key?('nameservers')
238
+ sorted_each(@config['nameservers']) do |name,ref|
239
+ #@config['nameservers'].each do |name,ref|
240
+ m = get_machine(ref)
241
+ if not m.nil?
242
+ machines << NameServer.new(name,m)
243
+ add_alias(name,m)
244
+ else
245
+ puts "ERROR: No such machine named #{ref}"
246
+ end
247
+ end
248
+ end
249
+ @nameservers = machines
250
+ end
251
+ def generate_cnames
252
+ @cnames = []
253
+ sorted_each(@config['cnames']) do |k,v|
254
+ #@config['cnames'].each do |k,v|
255
+ data = v
256
+ # Assume absolute domain name
257
+ # if value is not a machine name
258
+ unless @allnames.include?(v)
259
+ data = "#{v}."
260
+ end
261
+ @cnames << Record.new({
262
+ :name => k,
263
+ :type => 'CNAME',
264
+ :data => data
265
+ })
266
+ end
267
+ @cnames
268
+ end
269
+ def generate_records
270
+ @records = []
271
+ if @config.has_key?('records')
272
+ @config['records'].each do |record|
273
+ @records << Record.new(symbolize(record))
274
+ end
275
+ end
276
+ @records
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,28 @@
1
+ require 'yaml'
2
+ require 'redzone/zone'
3
+
4
+ module RedZone
5
+ # Contains zone configurations
6
+ class ZoneConfig
7
+ # Return the list of Zone objects
8
+ # @return [Array<Zone>] zone
9
+ attr_reader :zones
10
+ # Return the list of ArpaNetwork objects
11
+ # @return [Array<ArpaNetwork>] arpa networks
12
+ attr_reader :arpas
13
+ def initialize(file)
14
+ config = YAML.load_file(file)
15
+ common = config['zones']['common']
16
+ @zones = []
17
+ @arpas = []
18
+ config['zones'].each do |z,c|
19
+ if z != 'common'
20
+ cfg = common.merge(c)
21
+ zone = Zone.new(z,cfg)
22
+ @zones << zone
23
+ @arpas.concat zone.generate_arpa_list()
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ require 'yaml'
2
+ require 'redzone/zone'
3
+
4
+ module RedZone
5
+ # Writes zone configurations to files
6
+ class ZonefileWriter
7
+ # Constructs a ZonefileWriter
8
+ def initialize(zone_config)
9
+ @config = zone_config
10
+ end
11
+ # Write the zone database files to the target folder
12
+ # @param [Pathname] target Target directory
13
+ def write_zones(target)
14
+ raise ArgumentError, "Directory #{target} does not exist" unless target.exist?
15
+ @config.zones.each do |z|
16
+ with_file(target,z.name) { |io| z.write(io) }
17
+ end
18
+ @config.arpas.each do |a|
19
+ with_file(target,a.name) {|io| a.write(io)}
20
+ end
21
+ end
22
+ private
23
+ def with_file(target,name,&block)
24
+ filename = target + "#{name}.db"
25
+ File.open(filename,"w") do |file|
26
+ block.call(file)
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/redzone.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'redzone/cli'
2
+ require 'redzone/version'
data/redzone.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'redzone/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ # Metadata
8
+ spec.name = 'redzone'
9
+ spec.version = RedZone::VERSION
10
+ spec.authors = ['Justen Walker']
11
+ spec.email = %w(justen.walker@gmail.com)
12
+ spec.homepage = "https://github.com/justenwalker/redzone"
13
+ spec.summary = 'RedZone - Automatically generate BIND zone files'
14
+ spec.description = <<-eos.gsub(/^ +/,'')
15
+ RedZone is a command-line too that can generate bind zone
16
+ files and configuration from yaml syntax.
17
+ eos
18
+ spec.license = "MIT"
19
+
20
+
21
+ spec.files = `git ls-files`.split($/)
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ["lib"]
25
+
26
+ # Dependencies
27
+ spec.required_ruby_version = '>= 1.8.7'
28
+ spec.add_runtime_dependency 'thor', '~> 0.18.1'
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.5"
31
+ spec.add_development_dependency "rake"
32
+ end
data/redzone.rb ADDED
@@ -0,0 +1,279 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'tsort'
5
+
6
+ class String
7
+ def unindent
8
+ gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, "")
9
+ end
10
+ end
11
+ module RedZone
12
+ class ZoneConfig
13
+ include TSort
14
+ def initialize(zonefile)
15
+ @cfg = YAML.load_file(zonefile)
16
+ @zones = Hash.new {|h,k| h[k] = []}
17
+ @machines = []
18
+ @ips = []
19
+ @cfg['zones'].each do |k,v|
20
+ add(Zone.new(k,v))
21
+ end
22
+ end
23
+ def add(zone)
24
+ @zones[zone.name] = zone
25
+ end
26
+ def tsort_each_node(&block)
27
+ @zones.each_key(&block)
28
+ end
29
+ def tsort_each_child(node, &block)
30
+ @zones[node].dependencies.each(&block) if @zones.has_key?(node)
31
+ end
32
+ def domains
33
+ d = []
34
+ @zones.each do |k,v|
35
+ d << v.domains
36
+ end
37
+ d.flatten
38
+ end
39
+ def setup!
40
+ self.tsort.each do |z|
41
+ zone = @zones[z]
42
+ zone.dependencies.each do |dep|
43
+ zone.inherit!(@zones[dep])
44
+ end
45
+ end
46
+ end
47
+ def generate_domain(domain)
48
+ io = StringIO.new
49
+ @zones.each do |k,v|
50
+ if v.domains.include?(domain)
51
+ io << v.generate_zonefile(domain)
52
+ end
53
+ end
54
+ io.string
55
+ end
56
+ end
57
+ class Zone
58
+ attr_reader :name,:dependencies,:config
59
+ def inherit!(zone)
60
+ unless @inherited.include?(zone.name)
61
+ @config = zone.config.merge(@config)
62
+ @inherited << zone.name
63
+ end
64
+ end
65
+ def initialize(name,config)
66
+ @inherited = []
67
+ @name = name
68
+ @config = config
69
+ @dependencies = []
70
+ if @config.has_key?('inherits')
71
+ @dependencies << @config['inherits']
72
+ @dependencies = @dependencies.flatten
73
+ end
74
+ end
75
+ def domains
76
+ @config['domains'] || []
77
+ end
78
+ def generate_conffile(domain,datafile,masters=[])
79
+ io = StringIO.new
80
+ write_conffile(io,domain,datafile,masters)
81
+ io.string
82
+ end
83
+ def write_conffile(io,domain,datafile,masters=[])
84
+ if not masters.empty?
85
+ masterlist = masters.join(" ; ")
86
+ io << <<-eos.unindent
87
+ zone "#{domain}" in {
88
+ type slave;
89
+ file "#{datafile}";
90
+ masters { #{masterlist} ; } ;
91
+ }
92
+ eos
93
+ else
94
+ io << <<-eos.unindent
95
+ zone "#{domain}" in {
96
+ type master;
97
+ file "#{datafile}";
98
+ }
99
+ eos
100
+ end
101
+ end
102
+ def generate_zonefile(domain)
103
+ io = StringIO.new
104
+ write_zonefile(io,domain)
105
+ io.string
106
+ end
107
+ def write_zonefile(io,domain)
108
+ io << generate_soa(domain)
109
+ io << generate_default
110
+ io << generate_dns
111
+ io << generate_mail
112
+ io << generate_machines
113
+ io << generate_names
114
+ io << generate_aliases
115
+ io << generate_wildcard
116
+ end
117
+
118
+ private
119
+ def all_machines
120
+ machines = []
121
+ @config['machines'].each do |name,cfg|
122
+ machines << Machine.new(name,cfg)
123
+ end
124
+ machines
125
+ end
126
+ def get_machine(name)
127
+ Machine.new(name,@config['machines'][name])
128
+ end
129
+ def generate_soa(domain)
130
+ lifetimes = @config['lifetimes']
131
+ ttl = lifetimes['ttl']
132
+ hostmaster = @config['hostmaster']
133
+ now = Time.now.to_i
134
+ refresh = lifetimes['refresh']
135
+ re = lifetimes['retry']
136
+ expire = lifetimes['expire']
137
+ negative = lifetimes['negative']
138
+ <<-eos.unindent
139
+ $ORIGIN #{domain}.
140
+ $TTL #{ttl} ; queries are cached for this long
141
+ @ IN SOA ns1 #{hostmaster} (
142
+ #{now} ; Date $time
143
+ #{refresh} ; slave queries for refresh this often
144
+ #{re} ; slave retries refresh this often after failure
145
+ #{expire} ; slave expires after this long if not refreshed
146
+ #{negative} ; errors are cached for this long
147
+ )
148
+
149
+ eos
150
+ end
151
+ def generate_record(name,type,detail,value,comment=nil)
152
+ suffix = " ; #{comment}" unless comment.nil?
153
+ "%-20s IN %-8s %-6s %s%s\n" % [name, type, detail, value, suffix || ""]
154
+ end
155
+ def generate_machine(name,machine)
156
+ comment = "Machine #{machine}" unless name == machine
157
+ if machine.is_a? String
158
+ machine = get_machine(machine)
159
+ end
160
+ io = StringIO.new
161
+ io << generate_record(name,"A","",machine.ipv4,comment) if machine.ipv4?
162
+ io << generate_record(name,"AAAA","",machine.ipv6,comment) if machine.ipv6?
163
+ io.string
164
+ end
165
+ def generate_default
166
+ io = StringIO.new
167
+ if @config['default']
168
+ io << "; Primary name records for unqualfied domain\n"
169
+ io << generate_machine("@",@config['default'])
170
+ io << "\n"
171
+ end
172
+ io.string
173
+ end
174
+ def generate_dns
175
+ io = StringIO.new
176
+ if @config.has_key?('dns')
177
+ io << ": DNS Server Records\n"
178
+ dns_keys = @config['dns'].keys.sort
179
+ dns_keys.each do |k|
180
+ io << generate_record("@","NS","",k)
181
+ end
182
+ dns_keys.each do |k|
183
+ machine = @config['dns'][k]
184
+ io << generate_machine(k,machine)
185
+ end
186
+ io << "\n"
187
+ end
188
+ io.string
189
+ end
190
+ def generate_mail
191
+ io = StringIO.new
192
+ if @config.has_key?('mail')
193
+ io << "; Email Servers\n"
194
+ mail_keys = @config['mail'].keys.sort
195
+ mail_keys.each do |k|
196
+ mail = @config['mail'][k]
197
+ priority = mail['priority']
198
+ io << generate_record("@","MX",priority,k)
199
+ end
200
+ mail_keys.each do |k|
201
+ mail = @config['mail'][k]
202
+ machine = mail['machine']
203
+ io << generate_machine(k,machine)
204
+ end
205
+ io << "\n"
206
+ end
207
+ io.string
208
+ end
209
+ def generate_machines
210
+ io = StringIO.new
211
+ io << "; Primary Machine Names\n"
212
+ all_machines.each do |machine|
213
+ io << generate_machine(machine.name,machine)
214
+ end
215
+ io << "\n"
216
+ io.string
217
+ end
218
+ def generate_names
219
+ io = StringIO.new
220
+ if @config.has_key?('mail')
221
+ io << "; Extra Names\n"
222
+ name_keys = @config['names'].keys.sort
223
+ name_keys.each do |name|
224
+ machine = @config['names'][name]
225
+ io << generate_machine(name,machine)
226
+ end
227
+ io << "\n"
228
+ end
229
+ io.string
230
+ end
231
+ def generate_aliases
232
+ io = StringIO.new
233
+ if @config.has_key?('mail')
234
+ io << "; Extra Names\n"
235
+ cname_keys = @config['aliases'].keys.sort
236
+ cname_keys.each do |cname|
237
+ value = @config['aliases'][cname]
238
+ io << generate_record(cname,"CNAME","",value)
239
+ end
240
+ io << "\n"
241
+ end
242
+ io.string
243
+ end
244
+ def generate_wildcard
245
+ io = StringIO.new
246
+ if @config['wildcard']
247
+ io << "; Wildcard\n"
248
+ io << generate_machine("*",@config['wildcard'])
249
+ io << "\n"
250
+ end
251
+ io.string
252
+ end
253
+ end
254
+ class Machine
255
+ attr_reader :name,:ipv4,:ipv6
256
+ def initialize(name,config)
257
+ @name = name
258
+ @ipv4 = config['ipv4']
259
+ @ipv6 = config['ipv6']
260
+ end
261
+ def ipv4?
262
+ not @ipv4.nil?
263
+ end
264
+ def ipv6?
265
+ not @ipv6.nil?
266
+ end
267
+ def to_s
268
+ @name
269
+ end
270
+ end
271
+ end
272
+
273
+ zonecfg = RedZone::ZoneConfig.new 'zones.yml'
274
+ zonecfg.setup!
275
+ zonecfg.domains.each do |d|
276
+ puts zonecfg.generate_domain(d)
277
+ end
278
+
279
+
@@ -0,0 +1,30 @@
1
+ dir = File.expand_path(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift File.join(dir, 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+
7
+ Bundler.require :default, :test
8
+
9
+ require 'pathname'
10
+ require 'tmpdir'
11
+
12
+ Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour|
13
+ require behaviour.relative_path_from(Pathname.new(dir))
14
+ end
15
+
16
+ def root_path
17
+ Pathname.new(File.expand_path(File.join(__FILE__, '..', '..')))
18
+ end
19
+
20
+ def fixture_path
21
+ root_path + 'spec' + 'fixtures'
22
+ end
23
+
24
+ def resources_path
25
+ root_path + 'resources'
26
+ end
27
+
28
+ RSpec.configure do |config|
29
+ config.mock_with :mocha
30
+ end