Created
September 4, 2024 11:24
-
-
Save mgedmin/e446fa30c8a02f4a51fa5d1ebdec9ab6 to your computer and use it in GitHub Desktop.
Managing DNS servers with Ansible
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# filter_plugins/dns.py | |
import dns.zone # pip install dnspython or pipx inject ansible dnspython | |
import dns.resolver | |
import dns.flags | |
def get_dns_serial(zone, ns=None): | |
resolver = dns.resolver.Resolver(configure=True) | |
resolver.use_edns(0, ednsflags=dns.flags.DO, payload=4096) | |
if ns: | |
resolver.nameservers = [ | |
dns.resolver.query(ns)[0].address | |
] | |
answer = resolver.query(zone, 'SOA', rdclass='IN') | |
return answer[0].serial | |
def get_dns_serials(nameservers, zone): | |
return { | |
ns: get_dns_serial(zone, ns) | |
for ns in nameservers | |
} | |
def parse_zone(zone_text, origin=None): | |
"""Parse a DNS zone file.""" | |
zone = dns.zone.from_text(zone_text, origin=origin) | |
soa = zone.find_rdataset('@', 'SOA')[0] | |
serial = soa.serial | |
primary_ns = soa.mname.derelativize(zone.origin).to_text() | |
nameservers = [ | |
record.to_text(zone.origin, relativize=False) | |
for record in zone.find_rdataset('@', 'NS') | |
] | |
return dict(serial=serial, primary_ns=primary_ns, | |
nameservers=nameservers) | |
class FilterModule(object): | |
def filters(self): | |
return { | |
'get_dns_serial': get_dns_serial, | |
'get_dns_serials': get_dns_serials, | |
'parse_zone': parse_zone, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# roles/dns-server/defaults/main.yml | |
--- | |
# List of DNS zones, e.g. | |
# dns_zones: | |
# - example.com | |
# - example.net | |
# Each should have a corresponding zone file in files/db/dns.{{ zone }} | |
dns_zones: [] | |
# List of domains for which this server is a secondary NS. | |
# Each list item needs to specify three things: the zone, the IP of | |
# the primary NS, and, optionally, the name of the primary NS. | |
# YAML mapping syntax is abused for this: the key is the zone name, | |
# the value contains the IP and the hostname, separated by spaces. | |
# Example: | |
# secondary_ns_for: | |
# - example.com: 10.10.10.10 ns.example.com | |
# - example.net: 192.168.1.1 | |
secondary_ns_for: {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# roles/dns-server/handlers/main.yml | |
--- | |
- name: touch changed zone files | |
# workaround for https://github.com/ansible/ansible/issues/83013 | |
file: path=/etc/bind/db.{{ item }} state=touch | |
with_items: "{{ zone_file_result.results | selectattr('changed') | map(attribute='item') }}" | |
- name: reload bind9 | |
service: name=bind9 state=reloaded |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# roles/dns-server/tasks/check.yml | |
--- | |
- name: compare zone files | |
copy: src=dns/db.{{ item }} dest=/etc/bind/db.{{ item }} | |
check_mode: yes | |
with_items: "{{ dns_zones }}" | |
register: zone_result | |
tags: [ bind, check ] | |
- name: get current serial | |
assert: | |
quiet: yes | |
that: | |
- new_serial|int > current_serial|int | |
msg: > | |
{{ zone }} new serial ({{ new_serial }}) must be greater than the current | |
serial ({{ current_serial }}) | |
vars: | |
zone: "{{ item.item }}" | |
abs_zone: "{{ zone }}." | |
zone_file: "dns/db.{{ zone }}" | |
new_zone: "{{ lookup('file', zone_file)|parse_zone(abs_zone) }}" | |
new_serial: "{{ new_zone.serial }}" | |
primary_ns: "{{ ansible_fqdn }}" | |
current_serial: "{{ lookup('dig', abs_zone, 'qtype=SOA', '@' ~ primary_ns).split()[2]|int }}" | |
when: item.changed | |
with_items: "{{ zone_result.results }}" | |
loop_control: | |
label: "{{ zone }}" | |
tags: [ bind, check ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# roles/dns-server/tasks/main.yml | |
--- | |
- name: install bind9 | |
apt: name=bind9 state=present | |
tags: apt | |
- import_tasks: check.yml | |
tags: [ bind, check ] | |
- name: zone files | |
copy: src=dns/db.{{ item }} dest=/etc/bind/db.{{ item }} | |
with_items: "{{ dns_zones }}" | |
register: zone_file_result | |
notify: | |
- touch changed zone files | |
# TBH I'm not sure this is needed | |
- reload bind9 | |
tags: bind | |
- name: /etc/bind/named.conf.local | |
template: dest=/etc/bind/named.conf.local src=named.conf.local.j2 | |
notify: | |
- reload bind9 | |
tags: bind | |
- name: make sure bind9 is enabled | |
service: name=bind9 state=started | |
tags: bind | |
- meta: flush_handlers | |
- import_tasks: validate.yml | |
ignore_errors: "{{ ansible_check_mode }}" | |
tags: [ bind, validate ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# roles/dns-server/tasks/validate.yml | |
--- | |
- name: validate DNS servers | |
assert: | |
that: | |
- parsed_zone.primary_ns == primary_ns | |
- primary_ns in parsed_zone.nameservers | |
- dns_serial == zone_serial | |
- all_serials.values()|select('!=', zone_serial|int)|list == [] | |
msg: | | |
Expected primary_ns to be '{{ primary_ns }}', it was '{{ parsed_zone.primary_ns }}'. | |
Serial in zone file is {{ zone_serial }}, {{ primary_ns }} reports {{ dns_serial }}. | |
Nameservers reported in the zone file are {{ parsed_zone.nameservers }}. | |
Serials reported by the nameservers are {{ all_serials }}. | |
quiet: yes | |
success_msg: | | |
Expected primary_ns to be '{{ primary_ns }}', it was '{{ parsed_zone.primary_ns }}'. | |
Serial in zone file is {{ zone_serial }}, {{ primary_ns }} reports {{ dns_serial }}. | |
Nameservers reported in the zone file are {{ parsed_zone.nameservers }}. | |
Serials reported by the nameservers are {{ all_serials }}. | |
vars: | |
zone: "{{ item }}" | |
abs_zone: "{{ zone }}." | |
zone_file: "dns/db.{{ zone }}" | |
parsed_zone: "{{ lookup('file', zone_file)|parse_zone(abs_zone) }}" | |
zone_serial: "{{ parsed_zone.serial }}" | |
primary_ns: "{{ ansible_fqdn }}." | |
dns_serial: "{{ abs_zone|get_dns_serial(primary_ns) }}" | |
all_serials: "{{ parsed_zone.nameservers|get_dns_serials(abs_zone) }}" | |
with_items: "{{ dns_zones }}" | |
register: validation_result | |
tags: [ bind, validate ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// roles/dns-server/templates/named.conf.local.j2 | |
// | |
// Do any local configuration here | |
// | |
// Consider adding the 1918 zones here, if they are not used in your | |
// organization | |
//include "/etc/bind/zones.rfc1918"; | |
{% for zone in dns_zones %} | |
zone "{{ zone }}" { | |
type master; | |
file "/etc/bind/db.{{ zone }}"; | |
}; | |
{% endfor %} | |
{% for zoneinfo in secondary_ns_for %} | |
{% set zone = zoneinfo.keys()|first %} | |
{% set master = zoneinfo.values()|first %} | |
{% set master_ip = master.partition(' ')[0] %} | |
{% set master_name = master.partition(' ')[-1].strip() %} | |
zone "{{ zone }}" { | |
type slave; | |
file "db.{{ zone }}"; | |
{% if master_name %} | |
masters { {{ master_ip }}; }; // {{ master_name }} | |
{% else %} | |
masters { {{ master_ip }}; }; | |
{% endif %} | |
}; | |
{% endfor %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment