# frozen_string_literal: true # lib/facter/dns_records.rb # # Reports this host's expected DNS records (assembled by profiles::dns::updater # into its records file) versus what is currently deployed on the authoritative # server, so puppet can detect drift and re-apply. # # Structured value: # { server, count, expected => [{zone,fqdn,type,ttl,value}], in_sync, # drift => [{...,deployed => [...]}] } # Helpers for the dns_records fact. module DnsRecordsFact RECORDS_FILE = '/var/lib/dns-updater/records' SERVER_FILE = '/var/lib/dns-updater/server' module_function # normalise a value for comparison: strip, drop trailing dot, downcase def norm(value) value.to_s.strip.chomp('.').downcase end def server File.exist?(SERVER_FILE) ? File.read(SERVER_FILE).strip : nil end # a name relative to a zone (or @) as a fully-qualified name def to_fqdn(name, zone) return "#{zone}." if name.to_s.empty? || name == '@' "#{name}.#{zone}." end # parse one "zone|name|type|ttl|value" line into a record hash (nil to skip) def parse_line(line) line = line.strip return nil if line.empty? || line.start_with?('#') zone, name, type, ttl, value = line.split('|', 5) return nil unless zone && type && value { 'zone' => zone, 'fqdn' => to_fqdn(name, zone), 'type' => type, 'ttl' => ttl, 'value' => value } end # parse the records file into record hashes def expected return [] unless File.exist?(RECORDS_FILE) File.readlines(RECORDS_FILE).filter_map { |line| parse_line(line) } end # the values currently deployed for a record, per the authoritative server def deployed(record, srv) cmd = ['dig', '+short', '+time=2', '+tries=1'] cmd << "@#{srv}" if srv && !srv.empty? cmd += [record['fqdn'], record['type']] out = Facter::Core::Execution.execute(cmd.join(' '), on_fail: '') out.to_s.split("\n").map { |line| norm(line) }.reject(&:empty?) end def report srv = server exp = expected drift = exp.filter_map do |record| dep = deployed(record, srv) record.merge('deployed' => dep) unless dep.include?(norm(record['value'])) end { 'server' => srv, 'count' => exp.length, 'expected' => exp, 'in_sync' => drift.empty?, 'drift' => drift } end end Facter.add(:dns_records) do confine kernel: 'Linux' setcode do File.exist?(DnsRecordsFact::RECORDS_FILE) ? DnsRecordsFact.report : nil end end # Convenience boolean for `if $facts['dns_records_insync']` guards. Facter.add(:dns_records_insync) do confine kernel: 'Linux' setcode do v = Facter.value(:dns_records) v.nil? ? nil : v['in_sync'] end end