prole/infrastructure/roles/samba_dns/tasks/ensure_cname.yml
chrisfu c0c43bcf6a fix(samba_dns): strip zone suffix correctly so internal A/CNAME/PTR records are created
Forward A/CNAME and reverse PTR records reported changed=0 / ok but never
resolved by short name on the Samba AD DC (myrddin.prole.org). Root cause was
regex escaping inside YAML folded block scalars (">-"): unlike double-quoted
scalars, block scalars do NOT process backslash escapes, so a pattern written
as '\\.' reached Jinja as a literal backslash + any-char and never matched.

Consequences:
  - regex_replace() never stripped the zone suffix, so records were created
    under their full FQDN (e.g. a record literally named "sg2428lp.prole.org"
    inside zone "prole.org"). `samba-tool dns query ... prole.org sg2428lp A`
    then returns WERR_DNS_ERROR_NAME_DOES_NOT_EXIST.
  - regex_findall() of existing values always returned [], so stale records
    were never pruned and idempotency only survived via RECORD_ALREADY_EXISTS.

The leading/trailing whitespace from the folded scalar was a red herring: the
command module's shlex tokenization collapses it, so a single-token name
survived intact. The escaping was the actual defect.

Fix:
  - Convert the templated set_fact expressions from ">-" block scalars to
    double-quoted single-line scalars (one consistent escaping convention,
    matching the already-working samba_target_zone line; also removes the
    stray whitespace).
  - Add an assertion that the computed record name is a clean, non-empty,
    whitespace-free token.
  - Add a post-apply verification re-query + assert that the expected
    value(s) are actually present, turning a silent no-op into a hard failure.

Applies to roles/samba_dns (ensure_a.yml, ensure_cname.yml) and
roles/samba_reverse_dns (ensure_ptr.yml).

Verified end-to-end through ansible-playbook against a stateful fake
samba-tool: correct short names (sg2428lp, git, @, registry), stale-value
removal, internal.prole.org zone routing, idempotent re-runs (changed=0),
and the verify-assert failing loudly when an add silently does not persist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:32:00 -07:00

72 lines
3.4 KiB
YAML

---
- name: Determine target zone
ansible.builtin.set_fact:
samba_target_zone: "{{ 'internal.' ~ prole_domain if item.fqdn.endswith('.internal.' ~ prole_domain) or item.fqdn == 'internal.' ~ prole_domain else prole_domain }}"
# See ensure_a.yml for why these are double-quoted scalars and not ">-" blocks:
# in a block scalar '\\.' reaches the regex as a literal backslash and never
# matches, leaving the zone suffix un-stripped and the record mis-named.
- name: Determine record name (relative label; '@' for the zone apex)
ansible.builtin.set_fact:
samba_dns_record_name: "{{ '@' if fqdn_clean == samba_target_zone else (fqdn_clean | regex_replace('\\.' ~ (samba_target_zone | regex_escape) ~ '$', '')) }}"
vars:
fqdn_clean: "{{ item.fqdn | regex_replace('\\.$', '') }}"
- name: Assert record name is a clean single token
ansible.builtin.assert:
that:
- samba_dns_record_name | length > 0
- samba_dns_record_name == (samba_dns_record_name | trim)
- "' ' not in samba_dns_record_name"
fail_msg: "Computed CNAME record name '{{ samba_dns_record_name }}' for {{ item.fqdn }} is empty or contains whitespace."
- name: Query existing CNAME records
ansible.builtin.command:
cmd: samba-tool dns query {{ samba_dns_server }} {{ samba_target_zone }} {{ samba_dns_record_name }} CNAME
-U Administrator --password={{ samba_dns_admin_pass }}
register: cname_query
changed_when: false
failed_when: false
- name: Parse existing CNAME record value
ansible.builtin.set_fact:
cname_existing_value: "{{ cname_query.stdout | default('') | regex_findall('CNAME:\\s+([A-Za-z0-9.-]+)') | map('regex_replace', '\\.$', '') | first | default('') }}"
- name: Remove stale CNAME record
ansible.builtin.command:
cmd: samba-tool dns delete {{ samba_dns_server }} {{ samba_target_zone }} {{ samba_dns_record_name }} CNAME {{ cname_existing_value }}
-U Administrator --password={{ samba_dns_admin_pass }}
when:
- cname_existing_value | length > 0
- cname_existing_value != (item.target | regex_replace('\\.$', ''))
- name: Add missing CNAME record
ansible.builtin.command:
cmd: samba-tool dns add {{ samba_dns_server }} {{ samba_target_zone }} {{ samba_dns_record_name }} CNAME {{ item.target }}
-U Administrator --password={{ samba_dns_admin_pass }}
register: cname_add
changed_when: cname_add.rc == 0
failed_when: >
cname_add.rc != 0 and
('WERR_DNS_ERROR_RECORD_ALREADY_EXISTS' not in (cname_add.stderr | default(''))) and
('Record already exists' not in (cname_add.stderr | default('')))
when: cname_existing_value != (item.target | regex_replace('\\.$', ''))
- name: Verify CNAME record resolves
ansible.builtin.command:
cmd: samba-tool dns query {{ samba_dns_server }} {{ samba_target_zone }} {{ samba_dns_record_name }} CNAME
-U Administrator --password={{ samba_dns_admin_pass }}
register: cname_verify
changed_when: false
failed_when: false
- name: Assert expected CNAME target is present
ansible.builtin.assert:
that:
- cname_verify_value == (item.target | regex_replace('\\.$', ''))
fail_msg: >-
CNAME {{ samba_dns_record_name }} in zone {{ samba_target_zone }} points to
'{{ cname_verify_value }}' but expected '{{ item.target | regex_replace('\\.$', '') }}' after apply.
vars:
cname_verify_value: "{{ cname_verify.stdout | default('') | regex_findall('CNAME:\\s+([A-Za-z0-9.-]+)') | map('regex_replace', '\\.$', '') | first | default('') }}"