redzone 0.0.1

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