diff --git a/plugins/modules/nsupdate_zone.py b/plugins/modules/nsupdate_zone.py index 61c26c0..34d6f79 100644 --- a/plugins/modules/nsupdate_zone.py +++ b/plugins/modules/nsupdate_zone.py @@ -1,9 +1,8 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- -# Copyright (c) 2026, Dan Kercher -# -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations @@ -18,10 +17,10 @@ description: - Apply all changes atomically using batched DNS UPDATE messages per RFC 2136. - Support for configurable ignore patterns (e.g., NS records, ACME challenges). - Efficient management of large zones with hundreds or thousands of records. -version_added: 10.7.0 +version_added: "1.0.0" +author: Dan Kercher (@dkercher) requirements: - dnspython -author: "Dan Kercher" extends_documentation_fragment: - community.general.attributes attributes: @@ -309,7 +308,6 @@ import re from binascii import Error as binascii_error from ansible.module_utils.basic import AnsibleModule -from ansible.utils.display import Display from ansible_collections.valid.nsupdate_zone.plugins.module_utils import deps @@ -328,9 +326,8 @@ with deps.declare("dnspython", url="https://github.com/rthalley/dnspython"): class DNSZoneManager: - def __init__(self, module, zone_config): + def __init__(self, module: AnsibleModule, zone_config: dict) -> None: self.module = module - self.display = Display() self.zone_name_str = zone_config['name'] self.records = zone_config['records'] @@ -374,7 +371,7 @@ class DNSZoneManager: self.current_zone = None self.soa_minimum_ttl = 3600 # Default, will be updated from SOA - def _resolve_server(self): + def _resolve_server(self) -> list[str]: """Resolve DNS server FQDN to IP addresses.""" server = self.dns_server @@ -411,7 +408,7 @@ class DNSZoneManager: except Exception as e: self.module.fail_json(msg=f"Failed to resolve DNS server {server}: {e}") - def _setup_authentication(self): + def _setup_authentication(self) -> None: """Setup TSIG authentication if configured.""" key_name = self.module.params['key_name'] key_secret = self.module.params['key_secret'] @@ -435,7 +432,7 @@ class DNSZoneManager: self.keyname = None self.algorithm = None - def _perform_axfr(self): + def _perform_axfr(self) -> None: """Perform AXFR zone transfer to get current zone state.""" for server_ip in self.server_ips: try: @@ -483,7 +480,7 @@ class DNSZoneManager: self.module.fail_json(msg=f"AXFR failed for {self.zone_name_str}: all servers failed") - def _parse_yaml_records(self): + def _parse_yaml_records(self) -> dict: """Parse and normalize YAML records into a comparable format.""" parsed_records = {} @@ -530,7 +527,7 @@ class DNSZoneManager: return parsed_records - def _validate_record_values(self, parsed_records): + def _validate_record_values(self, parsed_records: dict) -> None: """Validate record values if validation is enabled.""" if not self.validate_records: return @@ -572,7 +569,7 @@ class DNSZoneManager: msg=f"Invalid {record_type} record at {name_str}: {value} - {e}" ) - def _build_record_sets(self): + def _build_record_sets(self) -> dict: """Build comparable record sets from current zone.""" if not self.current_zone: return {} @@ -599,7 +596,7 @@ class DNSZoneManager: return record_sets - def _should_ignore_record(self, name, record_type): + def _should_ignore_record(self, name: dns.name.Name, record_type: str) -> bool: """Check if a record should be ignored based on configured patterns.""" # Check type ignore if record_type in self.ignore_types: @@ -613,7 +610,7 @@ class DNSZoneManager: return False - def _filter_ignored_records(self, record_sets): + def _filter_ignored_records(self, record_sets: dict) -> dict: """Remove ignored records from record sets.""" filtered = {} for key, record_data in record_sets.items(): @@ -622,7 +619,7 @@ class DNSZoneManager: filtered[key] = record_data return filtered - def _compute_changes(self, desired_records, current_records): + def _compute_changes(self, desired_records: dict, current_records: dict) -> dict: """Compute adds, deletes, and updates needed.""" changes = { 'adds': [], @@ -644,7 +641,7 @@ class DNSZoneManager: 'values': desired['values'] }) if self.module._verbosity >= 1 or self.module._diff: - self.display.vvv(f"[{self.zone_name_str}] Removed: {name_str} {record_type}") + self.module.debug(f"[{self.zone_name_str}] Removed: {name_str} {desired['type']}") else: # State is 'present' if key not in current_records: @@ -652,7 +649,7 @@ class DNSZoneManager: changes['adds'].append(desired) if self.module._verbosity >= 1 or self.module._diff: values_str = ', '.join(str(v) for v in desired['values']) - self.display.vvv(f"[{self.zone_name_str}] Added: {name_str} {record_type} {values_str}") + self.module.debug(f"[{self.zone_name_str}] Added: {name_str} {record_type} {values_str}") else: # Record exists - check if values differ current = current_records[key] @@ -661,11 +658,11 @@ class DNSZoneManager: if self.module._verbosity >= 1 or self.module._diff: before_values = ', '.join(str(v) for v in current['values']) after_values = ', '.join(str(v) for v in desired['values']) - self.display.vvv(f"[{self.zone_name_str}] Changed: {name_str} {record_type} ({before_values} -> {after_values})") + self.module.debug(f"[{self.zone_name_str}] Changed: {name_str} {record_type} ({before_values} -> {after_values})") else: # Record unchanged if self.module._verbosity >= 2: - self.display.vvvv(f"[{self.zone_name_str}] Skipped: {name_str} {record_type} (unchanged)") + self.module.debug(f"[{self.zone_name_str}] Skipped: {name_str} {record_type} (unchanged)") # Find deletes (records in current but not in desired, unless ignored) for key, current in current_records.items(): @@ -680,11 +677,11 @@ class DNSZoneManager: 'values': current['values'] }) if self.module._verbosity >= 1 or self.module._diff: - self.display.vvv(f"[{self.zone_name_str}] Removed: {name_str} {record_type}") + self.module.debug(f"[{self.zone_name_str}] Removed: {name_str} {record_type}") return changes - def _validate_cname_conflicts(self, desired_records): + def _validate_cname_conflicts(self, desired_records: dict) -> None: """Validate that CNAME records don't conflict with other record types.""" name_types = {} @@ -703,7 +700,7 @@ class DNSZoneManager: msg=f"CNAME conflict at {name.to_text()}: cannot have CNAME with other record types {types}" ) - def _apply_changes(self, changes): + def _apply_changes(self, changes: dict) -> dict: """Apply changes using a single batched UPDATE message.""" # Create update message update = dns.update.Update( @@ -776,7 +773,7 @@ class DNSZoneManager: self.module.fail_json(msg=f"UPDATE failed for {self.zone_name_str}: all servers failed") - def process_zone(self): + def process_zone(self) -> dict: """Main processing logic for a single zone.""" result = { 'zone': self.zone_name_str, @@ -842,7 +839,7 @@ class DNSZoneManager: result['failed'] = True return result - def _format_diff(self, changes): + def _format_diff(self, changes: dict) -> dict: """Format changes as a diff structure for diff mode.""" diff_before = {} diff_after = {} @@ -870,13 +867,14 @@ class DNSZoneManager: } -def process_single_zone(module, zone_config): +def process_single_zone(module: AnsibleModule, zone_config: dict) -> dict: """Process a single zone (for parallel execution).""" manager = DNSZoneManager(module, zone_config) return manager.process_zone() -def main(): +def main() -> None: + """Main entry point for the module.""" module = AnsibleModule( argument_spec=dict( zones=dict(