diff --git a/lib/puppet/provider/resource_record/resource_record.rb b/lib/puppet/provider/resource_record/resource_record.rb index 6dc4788..fb08f42 100644 --- a/lib/puppet/provider/resource_record/resource_record.rb +++ b/lib/puppet/provider/resource_record/resource_record.rb @@ -1,40 +1,202 @@ # frozen_string_literal: true require 'puppet/resource_api/simple_provider' - +require 'ipaddr' # Implementation for the resource_record type using the Resource API. class Puppet::Provider::ResourceRecord::ResourceRecord < Puppet::ResourceApi::SimpleProvider + def initialize + super() + system('rndc', 'dumpdb', '-zones') + # Have to wait to ensure file is actually populated...embarrassingly this was the source of much wheel spinning. + sleep(2) + Puppet.debug('Parsing dump for existing resource records...') + @records = [] + @heldptr = {} + currentzone = '' + # FIXME: location varies based on config/OS + unless File.exist?('/var/cache/bind/named_dump.db') + raise Puppet::Error, 'The named dump file does not exist in the expected location, cannot continue.' + end + File.readlines('/var/cache/bind/named_dump.db').each do |line| + if line[0] == ';' && line.length > 18 + currentzone = line[%r{(?:.*?')(.*?)\/}, 1] + if currentzone.respond_to?(:to_str); currentzone = currentzone.downcase end + Puppet.debug("current zone updated: #{currentzone}") + elsif line[0] != ';' + line = line.strip.split(' ', 5) + rr = {} + rr[:label] = line[0] + if rr[:label].respond_to?(:to_str); rr[:label] = rr[:label].downcase end + # Puppet.debug("----New RR---- label: #{rr[:label]}") + rr[:ttl] = line[1] + # Puppet.debug("RR TTL: #{rr[:ttl]}") + rr[:scope] = line[2] + # Puppet.debug("RR scope: #{rr[:scope]}") + rr[:type] = line[3] + # Puppet.debug("RR type: #{rr[:type]}") + rr[:data] = if line[4].respond_to?(:to_str) + line[4].tr('\"', '') + else + line[4] + end + # context.debug("RR data: #{rr[:data]}") + rr[:zone] = currentzone + '.' + # context.debug("RR zone: #{rr[:zone]}") + @records << { + title: "#{rr[:label]} #{rr[:zone]} #{rr[:type]} #{rr[:data]}", + ensure: 'present', + record: rr[:label].to_s, + zone: rr[:zone].to_s, + type: rr[:type].to_s, + data: rr[:data].to_s, + ttl: rr[:ttl].to_s, + } + end + end + # context.debug("#{records.inspect}") + end + def get(context) - context.debug('Returning pre-canned example data') - [ - { - name: 'foo', - ensure: 'present', - }, - { - name: 'bar', - ensure: 'present', - }, - ] + Puppet.debug("get called, context: #{context}") + if @records.empty? + initialize + end + @records end def create(context, name, should) + Puppet.debug("create called, context: #{context}") context.notice("Creating '#{name}' with #{should.inspect}") + + # I dislike having to send an individual nsupdate for each record, it'd be preferable to + # build a request for each managed zone on run, append all records we + # need to act on, then send a bulk nsupdate for each zone - this would require a legacy provider's flush operation + cmd = if should[:type] == 'TXT' + "echo 'zone #{should[:zone]} + update add #{should[:record]} #{should[:ttl]} #{should[:type]} \"#{should[:data]}\" + send + quit + ' | nsupdate -4 -l" + else + "echo 'zone #{should[:zone]} + update add #{should[:record]} #{should[:ttl]} #{should[:type]} #{should[:data]} + send + quit + ' | nsupdate -4 -l" + end + system(cmd) + + # FIXME: This will generate PTR records, but assumes the arpa zones are preexisting. + if should[:type] == 'A' + unless @heldptr.key? should[:data].to_sym + if should[:ptrhold] + context.debug("Adding sticky PTR entry for #{should[:data]}->#{should[:record]}") + @heldptr[should[:data].to_sym] = should[:record] + end + fqdn = should[:record] + if fqdn[fqdn.length - 1] != '.' + fqdn += should[:zone] + end + reverse = IPAddr.new(should[:data]).reverse + cmd = "echo 'update delete #{reverse} PTR + update add #{reverse} #{should[:ttl]} PTR #{fqdn} + send + quit + ' | nsupdate -4 -l" + system(cmd) + end + end + @records << { + title: "#{should[:record]} #{should[:zone]} #{should[:type]} #{should[:data]}", + ensure: 'present', + record: should[:record].to_s, + zone: should[:zone].to_s, + type: should[:type].to_s, + data: should[:data].to_s, + ttl: should[:ttl].to_s, + } end def update(context, name, should) - context.notice("Updating '#{name}' with #{should.inspect}") + Puppet.debug("update called, context: #{context}") + context.notice("Updating '#{name.inspect}' with #{should.inspect}") + cmd = if should[:type] == 'TXT' + "echo 'zone #{should[:zone]} + update delete #{name[:record]} #{name[:type]} #{name[:data]} + update add #{should[:record]} #{should[:ttl]} #{should[:type]} \"#{should[:data]}\" + send + quit + ' | nsupdate -4 -l" + else + "echo 'zone #{should[:zone]} + update delete #{name[:record]} #{name[:type]} #{name[:data]} + update add #{should[:record]} #{should[:ttl]} #{should[:type]} #{should[:data]} + send + quit + ' | nsupdate -4 -l" + end + system(cmd) + if should[:type] == 'A' + unless @heldptr.key? should[:data].to_sym + if should[:ptrhold] + context.debug("Adding sticky PTR entry for #{should[:data]}->#{should[:record]}") + @heldptr[should[:data].to_sym] = should[:record] + end + fqdn = should[:record] + if fqdn[fqdn.length - 1] != '.' + fqdn += should[:zone] + end + reverse = IPAddr.new(should[:data]).reverse + context.debug("fqdn: #{fqdn}") + context.debug("reverse: #{reverse}") + cmd = "echo 'update delete #{reverse} PTR + update add #{reverse} #{should[:ttl]} PTR #{fqdn} + send + quit + ' | nsupdate -4 -l" + system(cmd) + end + end + @records.reject! { |rr| rr[:title] == "#{name[:record]} #{name[:zone]} #{name[:type]} #{name[:data]}" } + @records << { + title: "#{should[:record]} #{should[:zone]} #{should[:type]} #{should[:data]}", + ensure: 'present', + record: should[:record].to_s, + zone: should[:zone].to_s, + type: should[:type].to_s, + data: should[:data].to_s, + ttl: should[:ttl].to_s, + } end def delete(context, name) + Puppet.debug("delete called, context: #{context}") context.notice("Deleting '#{name}'") + cmd = "echo 'zone #{name[:zone]} + update delete #{name[:record]} #{name[:type]} #{name[:data]} + send + quit + ' | nsupdate -4 -l" + system(cmd) + @records.reject! { |rr| rr[:title] == "#{name[:record]} #{name[:zone]} #{name[:type]} #{name[:data]}" } end - def canonicalize(_context, resources) + def canonicalize(context, resources) + Puppet.debug("canonicalize called, context: #{context}") resources.each do |r| - r[:record] = r[:record].downcase - r[:zone] = r[:zone].downcase - r[:type] = r[:type].upcase + context.debug(r.inspect) + if r[:record].respond_to?(:to_str) + r[:record] = r[:record].downcase.strip + end + if r[:zone].respond_to?(:to_str) + r[:zone] = r[:zone].downcase + end + if r[:type].respond_to?(:to_str) + r[:type] = r[:type].upcase + end + if r[:data].respond_to?(:to_str) + r[:data] = r[:data].tr('\"', '') + end end end end diff --git a/lib/puppet/type/resource_record.rb b/lib/puppet/type/resource_record.rb index 2f7b764..4fa1bf5 100644 --- a/lib/puppet/type/resource_record.rb +++ b/lib/puppet/type/resource_record.rb @@ -21,8 +21,12 @@ features: ['canonicalize'], title_patterns: [ { - desc: 'name, zone (everything after the first dot), space, type', - pattern: %r{^(?.*?[^.])\.(?.*[^ ]\.) +(?.*)$}, + desc: 'full name, space, zone (explicitly defined), space, type, space, data', + pattern: %r{^(?.*?\.) (?[^ ]*\.) +(?\w+) (?.*)$}, + }, + { + desc: 'full name, space, zone (explicitly defined), space, type', + pattern: %r{^(?.*?\.) (?[^ ]*\.) +(?\w+)$}, }, { desc: 'name and zone (everything after the first dot)', @@ -43,6 +47,12 @@ desc: 'Whether this resource record should be present or absent on the target system.', default: 'present', }, + ptrhold: { + type: 'Boolean', + desc: 'Make this record the only one used for an accompanying reverse record.', + behavior: :parameter, + default: false, + }, record: { type: 'String', desc: 'The name of the resource record, also known as the owner or label.', @@ -61,6 +71,7 @@ data: { type: 'String', desc: 'The data for the resource record.', + behavior: :namevar, }, ttl: { type: 'Optional[String]', diff --git a/manifests/install.pp b/manifests/install.pp index 6b527ee..21f3f25 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -7,27 +7,6 @@ class bind::install { assert_private() - if $bind::authoritative { - ensure_packages( - [ - 'g++', - 'make', - ], - { - ensure => installed, - before => Package['dnsruby'], - }, - ) - - ensure_packages( - 'dnsruby', - { - ensure => installed, - provider => puppet_gem, - }, - ) - } - if $bind::package_backport { require apt::backports } diff --git a/manifests/zone.pp b/manifests/zone.pp index 7b36d50..e37ce92 100644 --- a/manifests/zone.pp +++ b/manifests/zone.pp @@ -10,6 +10,8 @@ # # @param allow_update Which hosts are allowed to submit Dynamic DNS updates to the zone. # +# @param allow_query Hosts allowed to query the zone +# # @param also_notify list of IP addresses of name servers that are also sent NOTIFY messages # whenever a fresh copy of the zone is loaded, in addition to the servers listed in the zone’s NS # records. @@ -64,6 +66,7 @@ Pattern[/\.$/] $zone_name = $title, Optional[Array[Variant[Stdlib::Host, Stdlib::IP::Address]]] $allow_transfer = undef, Optional[Array[Variant[Stdlib::Host, Stdlib::IP::Address]]] $allow_update = undef, + Optional[Array[Variant[Stdlib::Host, Stdlib::IP::Address]]] $allow_query = undef, Optional[Array[Variant[Stdlib::Host, Stdlib::IP::Address]]] $also_notify = undef, Optional[Enum['allow', 'maintain', 'off']] $auto_dnssec = undef, Optional[Enum['IN', 'HS', 'hesiod', 'CHAOS']] $class = undef, @@ -107,6 +110,7 @@ 'zone_name' => $zone_name, 'allow_transfer' => $allow_transfer, 'allow_update' => $allow_update, + 'allow_query' => $allow_query, 'also_notify' => $also_notify, 'auto_dnssec' => $auto_dnssec, 'class' => $class, @@ -206,7 +210,7 @@ } $resource_records.each |$rrname, $attribs| { - resource_record { $rrname: + resource_record { "${attribs['record']} ${zone_name} ${attribs['type']} ${attribs['data']}": zone => $zone_name, * => $attribs, } diff --git a/spec/classes/bind_spec.rb b/spec/classes/bind_spec.rb index a10c4b7..d7ad6ed 100644 --- a/spec/classes/bind_spec.rb +++ b/spec/classes/bind_spec.rb @@ -217,28 +217,6 @@ end it { is_expected.to compile.with_all_deps } - - # dnsruby build dependencies - if os_facts[:os]['name'] == 'Debian' - [ - 'g++', - 'make', - ].each do |pkg| - it do - is_expected.to contain_package(pkg).with( - ensure: 'installed', - before: 'Package[dnsruby]', - ) - end - end - end - - it do - is_expected.to contain_package('dnsruby').with( - ensure: 'installed', - provider: 'puppet_gem', - ) - end end context 'with dev packages' do diff --git a/templates/etc/bind/named.conf.epp b/templates/etc/bind/named.conf.epp index 27aa0c3..3257dd8 100644 --- a/templates/etc/bind/named.conf.epp +++ b/templates/etc/bind/named.conf.epp @@ -60,6 +60,13 @@ options { <%- } -%> }; <%- } -%> + <%- if $options['allow_recursion'] { -%> + allow-recursion { + <%- $options['allow_recursion'].each |$address_match_list_element| { -%> + <%= $address_match_list_element -%>; + <%- } -%> + }; + <%- } -%> <%- if $options['allow-update'] { -%> allow-update { <%- $options['allow-update'].each |$address_match_list_element| { -%> @@ -96,6 +103,16 @@ options { <%- if $options['zone-statistics'] { -%> zone-statistics <%= $options['zone-statistics'] %>; <%- } -%> + <%- if $options['forward'] { -%> + forward <%= $options['forward'] %>; + <%- } -%> + <%- if $options['forwarders'] { -%> + forwarders { + <%- $options['forwarders'].each |$address_match_list_element| { -%> + <%= $address_match_list_element -%>; + <%- } -%> + }; + <%- } -%> }; <%- } -%> diff --git a/templates/zone.conf.epp b/templates/zone.conf.epp index b825786..54fb7e7 100644 --- a/templates/zone.conf.epp +++ b/templates/zone.conf.epp @@ -42,6 +42,13 @@ zone "<%= $zone_name %>" { <%- } -%> }; <%- } -%> + <%- if $allow_query { -%> + allow-query { + <%- $allow_query.each |$address_match_list_element| { -%> + <%= $address_match_list_element -%>; + <%- } -%> + }; + <%- } -%> <%- if $also_notify { -%> also-notify { <%- $also_notify.each |$address_match_list_element| { -%> @@ -69,7 +76,7 @@ zone "<%= $zone_name %>" { }; <%- } -%> <%- if $primaries { -%> - primaries { + masters { <%- $primaries.each |$primary| { -%> <%= $primary %>; <%- } -%> diff --git a/types/options.pp b/types/options.pp index c3a8b39..55c3ac4 100644 --- a/types/options.pp +++ b/types/options.pp @@ -7,6 +7,7 @@ type Bind::Options = Struct[{ Optional['allow-transfer'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], Optional['allow-update'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], + Optional['allow_recursion'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], Optional['allow-query'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], Optional['also-notify'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], Optional['auto-dnssec'] => Enum['allow', 'maintain', 'off'], @@ -15,4 +16,6 @@ Optional['key-directory'] => String[1], Optional['serial-update-method'] => Enum['date', 'increment', 'unixtime'], Optional['zone-statistics'] => Variant[Boolean, Stdlib::Yes_no, Enum['full', 'terse', 'none']], + Optional['forward'] => Enum['first', 'only'], + Optional['forwarders'] => Array[Variant[Stdlib::Host, Stdlib::IP::Address]], }]