mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	refactor(dns): replace sys-dns-parent-hosts with sys-dns-wildcards; emit only *.parent wildcards from CURRENT_PLAY_DOMAINS_ALL
Rename filter parent_build_records→wildcard_records; create only wildcard (*.parent) A/AAAA records (no base/apex); switch to CURRENT_PLAY_DOMAINS_ALL; update vars to SYN_DNS_WILDCARD_RECORDS; adjust role/task names, defaults, and docs; add unittest expecting *.a.b from www.a.b.example.com. See: https://chatgpt.com/share/68c35dc1-7170-800f-8fbe-772e61780597
This commit is contained in:
		
							
								
								
									
										20
									
								
								roles/sys-dns-wildcards/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								roles/sys-dns-wildcards/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # sys-dns-wildcards | ||||
|  | ||||
| Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) only for **parent hosts** (hosts that have children). | ||||
| The **apex** (SLD.TLD) is considered when computing parents, but **no base host** or `*.apex` record is created by this role. | ||||
|  | ||||
| Examples: | ||||
| - c.wiki.example.com  -> parent: wiki.example.com -> creates: `*.wiki.example.com` | ||||
| - a.b.example.com     -> parent: b.example.com    -> creates: `*.b.example.com` | ||||
| - example.com (apex)  -> used to detect parents, but **no** `example.com` or `*.example.com` record is created | ||||
|  | ||||
| ## Inputs | ||||
| - parent_dns_domains (list[str], optional): FQDNs to evaluate. If empty, the role flattens CURRENT_PLAY_DOMAINS_ALL. | ||||
| - PRIMARY_DOMAIN (apex), defaults_networks.internet.ip4, optional defaults_networks.internet.ip6 | ||||
| - Flags: | ||||
|   - parent_dns_enabled (bool, default: true) | ||||
|   - parent_dns_ipv6_enabled (bool, default: true) | ||||
|   - parent_dns_proxied (bool, default: false) | ||||
|  | ||||
| ## Usage | ||||
| - Include the role once after your constructor stage has set CURRENT_PLAY_DOMAINS_ALL. | ||||
							
								
								
									
										3
									
								
								roles/sys-dns-wildcards/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								roles/sys-dns-wildcards/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| parent_dns_proxied: true | ||||
| parent_dns_domains: [] | ||||
| parent_dns_ipv6_enabled: true | ||||
							
								
								
									
										157
									
								
								roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| from ansible.errors import AnsibleFilterError | ||||
| import ipaddress | ||||
|  | ||||
|  | ||||
| def _validate(d: str) -> None: | ||||
|     if not isinstance(d, str) or not d.strip() or d.startswith(".") or d.endswith(".") or ".." in d: | ||||
|         raise AnsibleFilterError(f"Invalid domain: {d!r}") | ||||
|  | ||||
|  | ||||
| def _depth(domain: str, apex: str) -> int: | ||||
|     dl, al = domain.split("."), apex.split(".") | ||||
|     if not domain.endswith(apex) or len(dl) <= len(al): | ||||
|         return 0 | ||||
|     return len(dl) - len(al) | ||||
|  | ||||
|  | ||||
| def _parent_of_child(domain: str, apex: str) -> str | None: | ||||
|     """For a child like a.b.example.com return b.example.com; else None (needs depth >= 2).""" | ||||
|     if not domain.endswith(apex): | ||||
|         return None | ||||
|     parts = domain.split(".") | ||||
|     apex_len = len(apex.split(".")) | ||||
|     if len(parts) <= apex_len + 1: | ||||
|         return None | ||||
|     return ".".join(parts[1:])  # drop exactly the left-most label | ||||
|  | ||||
|  | ||||
| def _flatten_domains_any_structure(domains_like) -> list[str]: | ||||
|     """ | ||||
|     Accepts CURRENT_PLAY_DOMAINS_ALL-like structures: | ||||
|       - dict values: str | list/tuple/set[str] | dict (one level deeper) | ||||
|     Returns unique, sorted host list. | ||||
|     """ | ||||
|     hosts: list[str] = [] | ||||
|  | ||||
|     def _add_any(x): | ||||
|         if x is None: | ||||
|             return | ||||
|         if isinstance(x, str): | ||||
|             hosts.append(x) | ||||
|             return | ||||
|         if isinstance(x, (list, tuple, set)): | ||||
|             for i in x: | ||||
|                 if not isinstance(i, str): | ||||
|                     raise AnsibleFilterError(f"Non-string hostname in list: {i!r}") | ||||
|                 hosts.append(i) | ||||
|             return | ||||
|         if isinstance(x, dict): | ||||
|             for v in x.values(): | ||||
|                 _add_any(v) | ||||
|             return | ||||
|         raise AnsibleFilterError(f"Unsupported value type: {type(x).__name__}") | ||||
|  | ||||
|     if not isinstance(domains_like, dict): | ||||
|         raise AnsibleFilterError("Expected a dict for CURRENT_PLAY_DOMAINS_ALL") | ||||
|     for v in domains_like.values(): | ||||
|         _add_any(v) | ||||
|     return sorted(set(hosts)) | ||||
|  | ||||
|  | ||||
| def _parents_from(domains: list[str], apex: str, *, min_child_depth: int) -> list[str]: | ||||
|     _validate(apex) | ||||
|     parents = set() | ||||
|     for d in domains: | ||||
|         _validate(d) | ||||
|         if not d.endswith(apex): | ||||
|             continue | ||||
|         if _depth(d, apex) >= min_child_depth: | ||||
|             p = _parent_of_child(d, apex) | ||||
|             if p: | ||||
|                 parents.add(p) | ||||
|     return sorted(parents) | ||||
|  | ||||
|  | ||||
| def _is_global(ip: str) -> bool: | ||||
|     try: | ||||
|         return ipaddress.ip_address(ip).is_global | ||||
|     except Exception: | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def _build_wildcard_records( | ||||
|     parents: list[str], | ||||
|     apex: str, | ||||
|     *, | ||||
|     ip4: str, | ||||
|     ip6: str | None, | ||||
|     proxied: bool, | ||||
|     ipv6_enabled: bool, | ||||
| ) -> list[dict]: | ||||
|     if not isinstance(parents, list): | ||||
|         raise AnsibleFilterError("parents must be list[str]") | ||||
|     _validate(apex) | ||||
|     if not ip4: | ||||
|         raise AnsibleFilterError("ip4 required") | ||||
|  | ||||
|     records: list[dict] = [] | ||||
|  | ||||
|     def _add(name: str, rtype: str, content: str): | ||||
|         records.append({ | ||||
|             "zone": apex, | ||||
|             "type": rtype, | ||||
|             "name": name, | ||||
|             "content": content, | ||||
|             "proxied": bool(proxied), | ||||
|             "ttl": 1, | ||||
|         }) | ||||
|  | ||||
|     for p in sorted(set(parents)): | ||||
|         rel = p[:-len(apex)-1] if p != apex else ""  # relative part; apex shouldn't produce wildcard | ||||
|         if not rel: | ||||
|             # Do NOT create *.apex here (explicitly excluded by requirement) | ||||
|             continue | ||||
|         wc = f"*.{rel}" | ||||
|         _add(wc, "A", str(ip4)) | ||||
|         if ipv6_enabled and ip6 and _is_global(str(ip6)): | ||||
|             _add(wc, "AAAA", str(ip6)) | ||||
|     return records | ||||
|  | ||||
|  | ||||
| def wildcard_records( | ||||
|     current_play_domains_all: dict, | ||||
|     apex: str, | ||||
|     ip4: str, | ||||
|     ip6: str | None = None, | ||||
|     proxied: bool = False, | ||||
|     explicit_domains: list[str] | None = None, | ||||
|     min_child_depth: int = 2, | ||||
|     ipv6_enabled: bool = True, | ||||
| ) -> list[dict]: | ||||
|     """ | ||||
|     Build only wildcard records for parents: | ||||
|       for each parent 'parent.apex' -> create '*.parent' A/AAAA. | ||||
|       No base parent records and no '*.apex' are created. | ||||
|     """ | ||||
|     # Source domains | ||||
|     if explicit_domains and len(explicit_domains) > 0: | ||||
|         domains = sorted(set(explicit_domains)) | ||||
|     else: | ||||
|         domains = _flatten_domains_any_structure(current_play_domains_all) | ||||
|  | ||||
|     parents = _parents_from(domains, apex, min_child_depth=min_child_depth) | ||||
|     return _build_wildcard_records( | ||||
|         parents, | ||||
|         apex, | ||||
|         ip4=ip4, | ||||
|         ip6=ip6, | ||||
|         proxied=proxied, | ||||
|         ipv6_enabled=ipv6_enabled, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class FilterModule(object): | ||||
|     def filters(self): | ||||
|         return { | ||||
|             "wildcard_records": wildcard_records, | ||||
|         } | ||||
							
								
								
									
										7
									
								
								roles/sys-dns-wildcards/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								roles/sys-dns-wildcards/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| galaxy_info: | ||||
|   author: "Kevin Veen-Birkenbach" | ||||
|   description: "Create Cloudflare wildcard DNS records (*.parent) for parent hosts; no base or *.apex records." | ||||
|   license: "Infinito.Nexus NonCommercial License" | ||||
|   min_ansible_version: "2.12" | ||||
|   galaxy_tags: [dns, cloudflare, automation] | ||||
| dependencies: [] | ||||
							
								
								
									
										9
									
								
								roles/sys-dns-wildcards/tasks/01_core.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								roles/sys-dns-wildcards/tasks/01_core.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| --- | ||||
| - name: "Apply Cloudflare DNS for parent domains" | ||||
|   include_role: | ||||
|     name: sys-dns-cloudflare-records | ||||
|   vars: | ||||
|     cloudflare_records: "{{ SYN_DNS_WILDCARD_RECORDS }}" | ||||
|   when: DNS_PROVIDER == 'cloudflare' | ||||
|  | ||||
| - include_tasks: utils/run_once.yml | ||||
							
								
								
									
										3
									
								
								roles/sys-dns-wildcards/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								roles/sys-dns-wildcards/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| - block: | ||||
|   - include_tasks: 01_core.yml | ||||
|   when: run_once_sys_dns_wildcards is not defined | ||||
							
								
								
									
										12
									
								
								roles/sys-dns-wildcards/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								roles/sys-dns-wildcards/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| SYN_DNS_WILDCARD_RECORDS: >- | ||||
|   {{ CURRENT_PLAY_DOMAINS_ALL | ||||
|       | wildcard_records( | ||||
|           PRIMARY_DOMAIN, | ||||
|           defaults_networks.internet.ip4, | ||||
|           (defaults_networks.internet.ip6 | default('')), | ||||
|           parent_dns_proxied, | ||||
|           (parent_dns_domains | default([])), | ||||
|           2, | ||||
|           parent_dns_ipv6_enabled | ||||
|         ) | ||||
|   }} | ||||
		Reference in New Issue
	
	Block a user