diff --git a/roles/powerdns-zone/README.md b/roles/powerdns-zone/README.md new file mode 100644 index 0000000..c549ade --- /dev/null +++ b/roles/powerdns-zone/README.md @@ -0,0 +1,32 @@ +# `famedly.dns.powerdns-zone` ansible role + +This role aims to bootstrap an empty zone into powerdns so that it can be +used immediately without needing to be created using powerdns-admin or +API calls. + +For this, the contents of the SOA record, the zone name and it's type +need to be known, optionally the associated powerdns account. + +## Configuration + +A sample configuration is given below for setting up `demo.famedly.de`: + +```yaml +- hosts: [dns_authorative] + roles: + - name: powerdns-zone + vars: + powerdns_zone_name: demo.famedly.de + powerdns_zone_type: "{{ 'MASTER' if 'dns_primary' in group_names else 'SLAVE' }}" + # Assuming the primary has a variable called `ipv4`, store that ip in the + # `master` column of the domain table of the secondaries + powerdns_zone_master_ip: "{{ groups.dns_primary[0].ipv4 }}" + powerdns_zone_account: famedlydemo + powerdns_zone_soa_content: "ns0.famedly.de admin.famedly.de 2022010101 10800 3600 604800 3600" + # Database configuration + postgres_zone_database_type: postgres + postgres_zone_database_user: pdns + postgres_zone_database_password: asdoifjqwiejüojsdvinoöioeawjsf + postgres_zone_database_name: pdns + postgres_zone_database_host: 127.0.0.1 +``` diff --git a/roles/powerdns-zone/defaults/main.yml b/roles/powerdns-zone/defaults/main.yml new file mode 100644 index 0000000..4da40fe --- /dev/null +++ b/roles/powerdns-zone/defaults/main.yml @@ -0,0 +1,16 @@ +--- + +powerdns_zone_name: ~ +powerdns_zone_type: ~ +powerdns_zone_account: ~ +powerdns_zone_metadata: + SOA-EDIT-API: DEFAULT + ALLOW-AXFR-FROM: AUTO-NS +powerdns_zone_master_ip: ~ +powerdns_zone_soa_content: ~ + +powerdns_zone_database_type: postgres +powerdns_zone_database_user: pdns +powerdns_zone_database_password: ~ +powerdns_zone_database_name: pdns +powerdns_zone_database_host: ~ diff --git a/roles/powerdns-zone/tasks/main.yml b/roles/powerdns-zone/tasks/main.yml new file mode 100644 index 0000000..e2491dc --- /dev/null +++ b/roles/powerdns-zone/tasks/main.yml @@ -0,0 +1,81 @@ +--- + +- name: Configure zone '{{ powerdns_zone_name }}' + postgresql_query: + login_host: "{{ powerdns_zone_database_host }}" + login_user: "{{ powerdns_zone_database_user }}" + login_password: "{{ powerdns_zone_database_password }}" + db: "{{ powerdns_zone_database_name }}" + query: >- + INSERT INTO domains (name, {{ 'master,' if powerdns_zone_master_ip else '' }} type, account) + VALUES ( '{{ powerdns_zone_name }}', {{ "'" + powerdns_zone_master_ip + "'," if powerdns_zone_master_ip else '' }} '{{ powerdns_zone_type }}', '{{ powerdns_zone_account }}' ) + ON CONFLICT ( name ) + DO UPDATE SET "type" = '{{ powerdns_zone_type }}', "account" = '{{ powerdns_zone_account }}' + WHERE domains.name = '{{ powerdns_zone_name }}' and + (domains.type != '{{ powerdns_zone_type }}' + or domains.account != '{{ powerdns_zone_account }}'); + SELECT * FROM domains WHERE name = '{{ powerdns_zone_name }}'; + when: powerdns_zone_database_type == 'postgres' + register: zone_upsert_result + +- name: Extract internal domain_id for zone + set_fact: + zone_domain_id: "{{ zone_upsert_result.query_result[0].id }}" + +# TODO: this breaks encoding arrays, which are encoded by +# using multiple (domain_id, kind) entries with different values +- name: Create unique index (domain_id, kind) on domainmetadata table + postgresql_idx: + login_host: "{{ powerdns_zone_database_host }}" + login_user: "{{ powerdns_zone_database_user }}" + login_password: "{{ powerdns_zone_database_password }}" + db: "{{ powerdns_zone_database_name }}" + table: domainmetadata + columns: + - domain_id + - kind + name: domainmetadata_domain_kind_uniq + type: btree + unique: yes + when: powerdns_zone_database_type == 'postgres' + +- name: Configure zone metadata for '{{ powerdns_zone_name }}' + postgresql_query: + login_host: "{{ powerdns_zone_database_host }}" + login_user: "{{ powerdns_zone_database_user }}" + login_password: "{{ powerdns_zone_database_password }}" + db: "{{ powerdns_zone_database_name }}" + query: >- + INSERT INTO domainmetadata (domain_id, kind, content) + VALUES ( '{{ zone_domain_id }}', '{{ item.key }}', '{{ item.value }}' ) + ON CONFLICT ( domain_id, kind ) + DO UPDATE SET "content" = '{{ item.value }}' + WHERE domainmetadata.domain_id = '{{ zone_domain_id }}' and domainmetadata.kind = '{{ item.key }}' and domainmetadata.content != '{{ item.value }}'; + loop: "{{ powerdns_zone_metadata | dict2items }}" + loop_control: { label: "{{ item.key }} = {{ item.value }}" } + when: powerdns_zone_database_type == 'postgres' + +- name: Check if SOA record for zone '{{ powerdns_zone_name }}' exists + postgresql_query: + login_host: "{{ powerdns_zone_database_host }}" + login_user: "{{ powerdns_zone_database_user }}" + login_password: "{{ powerdns_zone_database_password }}" + db: "{{ powerdns_zone_database_name }}" + query: >- + SELECT * FROM records + WHERE records.domain_id = '{{ zone_domain_id }}' + and records.name = '{{ powerdns_zone_name }}' + and records.type = 'SOA'; + changed_when: false + register: zone_records_type_soa + +- name: Configure SOA record for zone '{{ powerdns_zone_name }}' + postgresql_query: + login_host: "{{ powerdns_zone_database_host }}" + login_user: "{{ powerdns_zone_database_user }}" + login_password: "{{ powerdns_zone_database_password }}" + db: "{{ powerdns_zone_database_name }}" + query: >- + INSERT INTO records (domain_id, name, type, content, ttl, prio, disabled, auth) + VALUES ( '{{ zone_domain_id }}', '{{ powerdns_zone_name }}', 'SOA', '{{ powerdns_zone_soa_content }}', 3600, 0, false, true) + when: powerdns_zone_database_type == 'postgres' and zone_records_type_soa.rowcount|int == 0