#!/usr/bin/python # -*- coding: utf-8 -*- # Authors: # Rafael Guterres Jeffman <rjeffman@redhat.com> # # Copyright (C) 2020 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """DNS Record ansible-freeipa module.""" ANSIBLE_METADATA = { "metadata_version": "1.0", "supported_by": "community", "status": ["preview"], } DOCUMENTATION = """ --- module: ipadnsrecord short description: Manage FreeIPA DNS records description: Manage FreeIPA DNS records extends_documentation_fragment: - ipamodule_base_docs options: records: description: The list of user dns records dicts required: false options: name: description: The DNS record name to manage. aliases: ["record_name"] required: true zone_name: description: | The DNS zone name to which DNS record needs to be managed. Required if not provided globally. aliases: ["dnszone"] required: false record_type: description: The type of DNS record. choices: ["A", "AAAA", "A6", "AFSDB", "CERT", "CNAME", "DLV", "DNAME", "DS", "KX", "LOC", "MX", "NAPTR", "NS", "PTR", "SRV", "SSHFP", "TLSA", "TXT", "URI"] default: "A" record_value: description: Manage DNS record name with these values. required: false type: list record_ttl: description: Set the TTL for the record. required: false type: int del_all: description: Delete all associated records. required: false type: bool a_rec: description: Raw A record. required: false aliases: ["a_record"] aaaa_rec: description: Raw AAAA record. required: false aliases: ["aaaa_record"] a6_rec: description: Raw A6 record. required: false aliases: ["a6_record"] afsdb_rec: description: Raw AFSDB record. required: false aliases: ["afsdb_record"] cert_rec: description: Raw CERT record. required: false aliases: ["cert_record"] cname_rec: description: Raw CNAME record. required: false aliases: ["cname_record"] dlv_rec: description: Raw DLV record. required: false aliases: ["dlv_record"] dname_rec: description: Raw DNAM record. required: false aliases: ["dname_record"] ds_rec: description: Raw DS record. required: false aliases: ["ds_record"] kx_rec: description: Raw KX record. required: false aliases: ["kx_record"] loc_rec: description: Raw LOC record. required: false aliases: ["loc_record"] mx_rec: description: Raw MX record. required: false aliases: ["mx_record"] naptr_rec: description: Raw NAPTR record. required: false aliases: ["naptr_record"] ns_rec: description: Raw NS record. required: false aliases: ["ns_record"] ptr_rec: description: Raw PTR record. required: false aliases: ["ptr_record"] srv_rec: description: Raw SRV record. required: false aliases: ["srv_record"] sshfp_rec: description: Raw SSHFP record. required: false aliases: ["sshfp_record"] tlsa_rec: description: Raw TLSA record. required: false aliases: ["tlsa_record"] txt_rec: description: Raw TXT record. required: false aliases: ["txt_record"] uri_rec: description: Raw URI record. required: false aliases: ["uri_record"] ip_address: description: IP adresses for A or AAAA records. required: false type: string a_ip_address: description: IP adresses for A records. required: false type: string a_create_reverse: description: | Create reverse record for A records. There is no equivalent to remove reverse records. type: bool required: false aaaa_ip_address: description: IP adresses for AAAA records. required: false type: string aaaa_create_reverse: description: | Create reverse record for AAAA records. There is no equivalent to remove reverse records. type: bool required: false create_reverse: description: | Create reverse record for A or AAAA record types. There is no equivalent to remove reverse records. type: bool required: false aliases: ["reverse"] a6_data: description: A6 record data. required: false afsdb_subtype: description: AFSDB Subtype required: false type: int afsdb_hostname: description: AFSDB Hostname required: false type: string cert_type: description: CERT Certificate Type required: false type: int cert_key_tag: description: CERT Key Tag required: false type: int cert_algorithm: description: CERT Algorithm required: false type: int cert_certificate_or_crl: description: CERT Certificate or Certificate Revocation List (CRL). required: false type: string cname_hostname: description: A hostname which this alias hostname points to. required: false type: string dlv_key_tag: description: DS Key Tag required: false type: int dlv_algorithm: description: DLV Algorithm required: false type: int dlv_digest_type: description: DLV Digest Type required: false type: int dlv_digest: description: DLV Digest required: false type: string dname_target: description: DNAME Target required: false type: string ds_key_tag: description: DS Key Tag required: false type: int ds_algorithm: description: DS Algorithm required: false type: int ds_digest_type: description: DS Digest Type required: false type: int ds_digest: description: DS Digest required: false type: string kx_preference: description: | Preference given to this exchanger. Lower values are more preferred. required: false type: int kx_exchanger: description: A host willing to act as a key exchanger. required: false type: string loc_lat_deg: description: LOC Degrees Latitude required: false type: int loc_lat_min: description: LOC Minutes Latitude required: false type: int loc_lat_sec: description: LOC Seconds Latitude required: false type: float loc_lat_dir: description: LOC Direction Latitude required: false choices: ["N", "S"] loc_lon_deg: description: LOC Degrees Longitude required: false type: int loc_lon_min: description: LOC Minutes Longitude required: false type: int loc_lon_sec: description: LOC Seconds Longitude required: false type: float loc_lon_dir: description: LOC Direction Longitude required: false choices: ["E", "W"] loc_altitude: description: LOC Altitude required: false type: float loc_size: description: LOC Size required: false type: float loc_h_precision: description: LOC Horizontal Precision required: false type: float loc_v_precision: description: LOC Vertical Precision required: false type: float mx_preference: description: | Preference given to this exchanger. Lower values are more preferred. required: false type: int mx_exchanger: description: A host willing to act as a mail exchanger. required: false type: string naptr_order: description: NAPTR Order required: false type: int naptr_preference: description: NAPTR Preference required: false type: int naptr_flags: description: NAPTR Flags required: false type: string naptr_service: description: NAPTR Service required: false type: string naptr_regexp: description: NAPTR Regular Expression required: false type: string naptr_replacement: description: NAPTR Replacement required: false type: string ns_hostname: description: NS Hostname required: false type: string ptr_hostname: description: The hostname this reverse record points to. required: false type: string srv_priority: description: | Lower number means higher priority. Clients will attempt to contact the server with the lowest-numbered priority they can reach. required: false type: int srv_weight: description: Relative weight for entries with the same priority. required: false type: int srv_port: description: SRV Port required: false type: int srv_target: description: | The domain name of the target host or '.' if the service is decidedly not available at this domain. required: false type: string sshfp_algorithm: description: SSHFP Algorithm required: False type: int sshfp_fp_type: description: SSHFP Fingerprint Type required: False type: int sshfp_fingerprint: description: SSHFP Fingerprint required: False type: string txt_data: description: TXT Text Data required: false type: string tlsa_cert_usage: description: TLSA Certificate Usage required: false type: int tlsa_selector: description: TLSA Selector required: false type: int tlsa_matching_type: description: TLSA Matching Type required: false type: int tlsa_cert_association_data: description: TLSA Certificate Association Data required: false type: string uri_target: description: Target Uniform Resource Identifier according to RFC 3986. required: false type: string uri_priority: description: | Lower number means higher priority. Clients will attempt to contact the URI with the lowest-numbered priority they can reach. required: false type: int uri_weight: description: Relative weight for entries with the same priority. required: false type: int zone_name: description: | The DNS zone name to which DNS record needs to be managed. Required if not provided globally. aliases: ["dnszone"] required: false name: description: The DNS record name to manage. aliases: ["record_name"] required: true record_type: description: The type of DNS record. required: false choices: ["A", "AAAA", "A6", "AFSDB", "CERT", "CNAME", "DLV", "DNAME", "DS", "KX", "LOC", "MX", "NAPTR", "NS", "PTR", "SRV", "SSHFP", "TLSA", "TXT", "URI"] default: "A" record_value: description: Manage DNS record name with this values. required: false type: list record_ttl: description: Set the TTL for the record. required: false type: int del_all: description: Delete all associated records. required: false type: bool a_rec: description: Raw A record. required: false aliases: ["a_record"] aaaa_rec: description: Raw AAAA record. required: false aliases: ["aaaa_record"] a6_rec: description: Raw A6 record. required: false aliases: ["a6_record"] afsdb_rec: description: Raw AFSDB record. required: false aliases: ["afsdb_record"] cert_rec: description: Raw CERT record. required: false aliases: ["cert_record"] cname_rec: description: Raw CNAME record. required: false aliases: ["cname_record"] dlv_rec: description: Raw DLV record. required: false aliases: ["dlv_record"] dname_rec: description: Raw DNAM record. required: false aliases: ["dname_record"] ds_rec: description: Raw DS record. required: false aliases: ["ds_record"] kx_rec: description: Raw KX record. required: false aliases: ["kx_record"] loc_rec: description: Raw LOC record. required: false aliases: ["loc_record"] mx_rec: description: Raw MX record. required: false aliases: ["mx_record"] naptr_rec: description: Raw NAPTR record. required: false aliases: ["naptr_record"] ns_rec: description: Raw NS record. required: false aliases: ["ns_record"] ptr_rec: description: Raw PTR record. required: false aliases: ["ptr_record"] srv_rec: description: Raw SRV record. required: false aliases: ["srv_record"] sshfp_rec: description: Raw SSHFP record. required: false aliases: ["sshfp_record"] tlsa_rec: description: Raw TLSA record. required: false aliases: ["tlsa_record"] txt_rec: description: Raw TXT record. required: false aliases: ["txt_record"] uri_rec: description: Raw URI record. required: false aliases: ["uri_record"] ip_address: description: IP adresses for A ar AAAA. required: false type: string create_reverse: description: | Create reverse record for A or AAAA record types. There is no equivalent to remove reverse records. type: bool required: false aliases: ["reverse"] a_ip_address: description: IP adresses for A records. required: false type: string a_create_reverse: description: | Create reverse record for A records. There is no equivalent to remove reverse records. type: bool required: false aaaa_ip_address: description: IP adresses for AAAA records. required: false type: string aaaa_create_reverse: description: | Create reverse record for AAAA records. There is no equivalent to remove reverse records. type: bool required: false afsdb_subtype: description: AFSDB Subtype required: false type: int afsdb_hostname: description: AFSDB Hostname required: false type: string cert_type: description: CERT Certificate Type required: false type: int cert_key_tag: description: CERT Key Tag required: false type: int cert_algorithm: description: CERT Algorithm required: false type: int cert_certificate_or_crl: description: CERT Certificate/CRL required: false type: string cname_hostname: description: A hostname which this alias hostname points to. required: false type: string dlv_key_tag: description: DS Key Tag required: false type: int dlv_algorithm: description: DLV Algorithm required: false type: int dlv_digest_type: description: DLV Digest Type required: false type: int dlv_digest: description: DLV Digest required: false type: string dname_target: description: DNAME Target required: false type: string ds_key_tag: description: DS Key Tag required: false type: int ds_algorithm: description: DS Algorithm required: false type: int ds_digest_type: description: DS Digest Type required: false type: int ds_digest: description: DS Digest required: false type: string kx_preference: description: | Preference given to this exchanger. Lower values are more preferred. required: false type: int kx_exchanger: description: A host willing to act as a key exchanger. required: false type: string loc_lat_deg: description: LOC Degrees Latitude required: false type: int loc_lat_min: description: LOC Minutes Latitude required: false type: int loc_lat_sec: description: LOC Seconds Latitude required: false type: float loc_lat_dir: description: LOC Direction Latitude required: false choices: ["N", "S"] loc_lon_deg: description: LOC Degrees Longitude required: false type: int loc_lon_min: description: LOC Minutes Longitude required: false type: int loc_lon_sec: description: LOC Seconds Longitude required: false type: float loc_lon_dir: description: LOC Direction Longitude required: false choices: ["E", "W"] loc_altitude: description: LOC Altitude required: false type: float loc_size: description: LOC Size required: false type: float loc_h_precision: description: LOC Horizontal Precision required: false type: float loc_v_precision: description: LOC Vertical Precision required: false type: float mx_preference: description: | Preference given to this exchanger. Lower values are more preferred. required: false type: int mx_exchanger: description: A host willing to act as a mail exchanger. required: false type: string naptr_order: description: NAPTR Order required: false type: int naptr_preference: description: NAPTR Preference required: false type: int naptr_flags: description: NAPTR Flags required: false type: string naptr_service: description: NAPTR Service required: false type: string naptr_regexp: description: NAPTR Regular Expression required: false type: string naptr_replacement: description: NAPTR Replacement required: false type: string ns_hostname: description: NS Hostname required: false type: string ptr_hostname: description: The hostname this reverse record points to. required: false type: string srv_priority: description: | Lower number means higher priority. Clients will attempt to contact the server with the lowest-numbered priority they can reach. required: false type: int srv_weight: description: Relative weight for entries with the same priority. required: false type: int srv_port: description: SRV Port required: false type: int srv_target: description: | The domain name of the target host or '.' if the service is decidedly not available at this domain. required: false type: string sshfp_algorithm: description: SSHFP Algorithm required: false type: int sshfp_fp_type: description: SSHFP Fingerprint Type required: false type: int sshfp_fingerprint: description: SSHFP Fingerprint required: false type: string txt_data: description: TXT Text Data required: false type: string tlsa_cert_usage: description: TLSA Certificate Usage required: false type: int tlsa_selector: description: TLSA Selector required: false type: int tlsa_matching_type: description: TLSA Matching Type required: false type: int tlsa_cert_association_data: description: TLSA Certificate Association Data required: false type: string uri_target: description: Target Uniform Resource Identifier according to RFC 3986. required: false type: string uri_priority: description: | Lower number means higher priority. Clients will attempt to contact the URI with the lowest-numbered priority they can reach. required: false type: int uri_weight: description: Relative weight for entries with the same priority. required: false type: int state: description: State to ensure default: present choices: ["present", "absent"] author: - Rafael Guterres Jeffman """ EXAMPLES = """ # Ensure dns record is present - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: vm-001 zone_name: example.com record_type: 'AAAA' record_value: '::1' # Ensure that dns record exists with a TTL - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: host01 zone_name: example.com record_type: 'AAAA' record_value: '::1' record_ttl: 300 # Ensure that dns record exists with a reverse record - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: host02 zone_name: example.com record_type: 'AAAA' record_value: 'fd00::0002' create_reverse: yes # Ensure a PTR record is present - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: 5 zone_name: 2.168.192.in-addr.arpa record_type: 'PTR' record_value: 'internal.ipa.example.com' # Ensure a TXT record is present - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: _kerberos zone_name: example.com record_type: 'TXT' record_value: 'EXAMPLE.COM' # Ensure a SRV record is present - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: _kerberos._udp.example.com zone_name: example.com record_type: 'SRV' record_value: '10 50 88 ipa.example.com' # Ensure an MX record is present - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: '@' zone_name: example.com record_type: 'MX' record_value: '1 mailserver.example.com' # Ensure that dns record is absent - ipadnsrecord: ipaadmin_password: SomeADMINpassword name: host01 zone_name: example.com record_type: 'AAAA' record_value: '::1' state: absent """ RETURN = """ """ from ansible.module_utils._text import to_text from ansible.module_utils.ansible_freeipa_module import \ IPAAnsibleModule, is_ipv4_addr, is_ipv6_addr, ipalib_errors import dns.reversename import dns.resolver import six if six.PY3: unicode = str _SUPPORTED_RECORD_TYPES = [ "A", "AAAA", "A6", "AFSDB", "CERT", "CNAME", "DLV", "DNAME", "DS", "KX", "LOC", "MX", "NAPTR", "NS", "PTR", "SRV", "SSHFP", "TLSA", "TXT", "URI"] _RECORD_FIELDS = [ "a_rec", "aaaa_rec", "a6_rec", "afsdb_rec", "cert_rec", "cname_rec", "dlv_rec", "dname_rec", "ds_rec", "kx_rec", "loc_rec", "mx_rec", "naptr_rec", "ns_rec", "ptr_rec", "srv_rec", "sshfp_rec", "tlsa_rec", "txt_rec", "uri_rec" ] # The _PART_MAP structure maps ansible-freeipa attributes to their # FreeIPA API counterparts. The keys are also used to obtain a list # of all supported DNS record attributes. _PART_MAP = { 'a_ip_address': 'a_part_ip_address', 'a_create_reverse': 'a_extra_create_reverse', 'aaaa_ip_address': 'aaaa_part_ip_address', 'aaaa_create_reverse': 'aaaa_extra_create_reverse', 'a6_data': 'a6_part_data', 'afsdb_subtype': 'afsdb_part_subtype', 'afsdb_hostname': 'afsdb_part_hostname', 'cert_type': 'cert_part_type', 'cert_key_tag': 'cert_part_key_tag', 'cert_algorithm': 'cert_part_algorithm', 'cert_certificate_or_crl': 'cert_part_certificate_or_crl', 'cname_hostname': 'cname_part_hostname', 'dlv_algorithm': 'dlv_part_algorithm', 'dlv_digest': 'dlv_part_digest', 'dlv_digest_type': 'dlv_part_digest_type', 'dlv_key_tag': 'dlv_part_key_tag', 'dname_target': 'dname_part_target', 'ds_algorithm': 'ds_part_algorithm', 'ds_digest': 'ds_part_digest', 'ds_digest_type': 'ds_part_digest_type', 'ds_key_tag': 'ds_part_key_tag', 'kx_preference': 'kx_part_preference', 'kx_exchanger': 'kx_part_exchanger', "loc_lat_deg": "loc_part_lat_deg", "loc_lat_min": "loc_part_lat_min", "loc_lat_sec": "loc_part_lat_sec", "loc_lat_dir": "loc_part_lat_dir", "loc_lon_deg": "loc_part_lon_deg", "loc_lon_min": "loc_part_lon_min", "loc_lon_sec": "loc_part_lon_sec", "loc_lon_dir": "loc_part_lon_dir", "loc_altitude": "loc_part_altitude", "loc_size": "loc_part_size", "loc_h_precision": "loc_part_h_precision", "loc_v_precision": "loc_part_v_precision", "mx_preference": "mx_part_preference", "mx_exchanger": 'mx_part_exchanger', "naptr_order": "naptr_part_order", "naptr_preference": "naptr_part_preference", "naptr_flags": "naptr_part_flags", "naptr_service": "naptr_part_service", "naptr_regexp": "naptr_part_regexp", "naptr_replacement": "naptr_part_replacement", 'ns_hostname': 'ns_part_hostname', 'ptr_hostname': 'ptr_part_hostname', "srv_priority": "srv_part_priority", "srv_weight": "srv_part_weight", "srv_port": "srv_part_port", "srv_target": "srv_part_target", 'sshfp_algorithm': 'sshfp_part_algorithm', 'sshfp_fingerprint': 'sshfp_part_fingerprint', 'sshfp_fp_type': 'sshfp_part_fp_type', "tlsa_cert_usage": "tlsa_part_cert_usage", "tlsa_cert_association_data": "tlsa_part_cert_association_data", "tlsa_matching_type": "tlsa_part_matching_type", "tlsa_selector": "tlsa_part_selector", 'txt_data': 'txt_part_data', "uri_priority": "uri_part_priority", "uri_target": "uri_part_target", "uri_weight": "uri_part_weight" } # _RECORD_PARTS is a structure that maps the attributes that store # the DNS record in FreeIPA API to the parts and options available # for these records in the API. _RECORD_PARTS = { "arecord": ["a_part_ip_address", "a_extra_create_reverse"], "aaaarecord": [ "aaaa_part_ip_address", "aaaa_extra_create_reverse" ], "a6record": ["a6_part_data"], "afsdbrecord": ['afsdb_part_subtype', 'afsdb_part_hostname'], "certrecord": [ 'cert_part_type', 'cert_part_key_tag', 'cert_part_algorithm', 'cert_part_certificate_or_crl' ], "cnamerecord": ["cname_part_hostname"], "dlvrecord": [ 'dlv_part_key_tag', 'dlv_part_algorithm', 'dlv_part_digest_type', 'dlv_part_digest' ], "dnamerecord": ["dname_part_target"], "dsrecord": ['ds_part_key_tag', 'ds_part_algorithm', 'ds_part_digest_type', 'ds_part_digest'], "kxrecord": ['kx_part_preference', 'kx_part_exchanger'], "locrecord": [ "loc_part_lat_deg", "loc_part_lat_min", "loc_part_lat_sec", "loc_part_lat_dir", "loc_part_lon_deg", "loc_part_lon_min", "loc_part_lon_sec", "loc_part_lon_dir", "loc_part_altitude", "loc_part_size", "loc_part_h_precision", "loc_part_v_precision" ], "mxrecord": ['mx_part_preference', 'mx_part_exchanger'], "naptrrecord": [ "naptr_part_order", "naptr_part_preference", "naptr_part_flags", "naptr_part_service", "naptr_part_regexp", "naptr_part_replacement" ], "nsrecord": ["ns_part_hostname"], "ptrrecord": ["ptr_part_hostname"], "srvrecord": [ "srv_part_priority", "srv_part_weight", "srv_part_port", "srv_part_target", ], "sshfprecord": [ 'sshfp_part_algorithm', 'sshfp_part_fingerprint', 'sshfp_part_fp_type' ], "tlsarecord": [ "tlsa_part_cert_usage", "tlsa_part_cert_association_data", "tlsa_part_matching_type", "tlsa_part_selector" ], "txtrecord": ["txt_part_data"], "urirecord": ["uri_part_priority", "uri_part_target", "uri_part_weight"], } def configure_module(): """Configure ipadnsrecord ansible module variables.""" record_spec = dict( zone_name=dict(type='str', required=False, aliases=['dnszone']), record_type=dict(type='str', default="A", choices=["A", "AAAA", "A6", "AFSDB", "CERT", "CNAME", "DLV", "DNAME", "DS", "KX", "LOC", "MX", "NAPTR", "NS", "PTR", "SRV", "SSHFP", "TLSA", "TXT", "URI"]), record_value=dict(type='list', required=False), record_ttl=dict(type='int', required=False), del_all=dict(type='bool', required=False), a_rec=dict(type='list', required=False, aliases=['a_record']), aaaa_rec=dict(type='list', required=False, aliases=['aaaa_record']), a6_rec=dict(type='list', required=False, aliases=['a6_record']), afsdb_rec=dict(type='list', required=False, aliases=['afsdb_record']), cert_rec=dict(type='list', required=False, aliases=['cert_record']), cname_rec=dict(type='list', required=False, aliases=['cname_record']), dlv_rec=dict(type='list', required=False, aliases=['dlv_record']), dname_rec=dict(type='list', required=False, aliases=['dname_record']), ds_rec=dict(type='list', required=False, aliases=['ds_record']), kx_rec=dict(type='list', required=False, aliases=['kx_record']), loc_rec=dict(type='list', required=False, aliases=['loc_record']), mx_rec=dict(type='list', required=False, aliases=['mx_record']), naptr_rec=dict(type='list', required=False, aliases=['naptr_record']), ns_rec=dict(type='list', required=False, aliases=['ns_record']), ptr_rec=dict(type='list', required=False, aliases=['ptr_record']), srv_rec=dict(type='list', required=False, aliases=['srv_record']), sshfp_rec=dict(type='list', required=False, aliases=['sshfp_record']), tlsa_rec=dict(type='list', required=False, aliases=['tlsa_record']), txt_rec=dict(type='list', required=False, aliases=['txt_record']), uri_rec=dict(type='list', required=False, aliases=['uri_record']), ip_address=dict(type='str', required=False), create_reverse=dict(type='bool', required=False, aliases=['reverse']), a_ip_address=dict(type='str', required=False), a_create_reverse=dict(type='bool', required=False), aaaa_ip_address=dict(type='str', required=False), aaaa_create_reverse=dict(type='bool', required=False), a6_data=dict(type='str', required=False), afsdb_subtype=dict(type='int', required=False), afsdb_hostname=dict(type='str', required=False), cert_type=dict(type='int', required=False), cert_key_tag=dict(type='int', required=False), cert_algorithm=dict(type='int', required=False), cert_certificate_or_crl=dict(type='str', required=False), cname_hostname=dict(type='str', required=False), dlv_key_tag=dict(type='int', required=False), dlv_algorithm=dict(type='int', required=False), dlv_digest_type=dict(type='int', required=False), dlv_digest=dict(type='str', required=False), dname_target=dict(type='str', required=False), ds_key_tag=dict(type='int', required=False), ds_algorithm=dict(type='int', required=False), ds_digest_type=dict(type='int', required=False), ds_digest=dict(type='str', required=False), kx_preference=dict(type='int', required=False), kx_exchanger=dict(type='str', required=False), loc_lat_deg=dict(type='int', required=False), loc_lat_min=dict(type='int', required=False), loc_lat_sec=dict(type='float', required=False), loc_lat_dir=dict(type='str', required=False), loc_lon_deg=dict(type='int', required=False), loc_lon_min=dict(type='int', required=False), loc_lon_sec=dict(type='float', required=False), loc_lon_dir=dict(type='str', required=False), loc_altitude=dict(type='float', required=False), loc_size=dict(type='float', required=False), loc_h_precision=dict(type='float', required=False), loc_v_precision=dict(type='float', required=False), mx_preference=dict(type='int', required=False), mx_exchanger=dict(type='str', required=False), naptr_order=dict(type='int', required=False), naptr_preference=dict(type='int', required=False), naptr_flags=dict(type='str', required=False), naptr_service=dict(type='str', required=False), naptr_regexp=dict(type='str', required=False), naptr_replacement=dict(type='str', required=False), ns_hostname=dict(type='str', required=False), ptr_hostname=dict(type='str', required=False), srv_priority=dict(type='int', required=False), srv_weight=dict(type='int', required=False), srv_port=dict(type='int', required=False), srv_target=dict(type='str', required=False), sshfp_algorithm=dict(type='int', required=False), sshfp_fingerprint=dict(type='str', required=False), sshfp_fp_type=dict(type='int', required=False), tlsa_cert_usage=dict(type='int', required=False), tlsa_cert_association_data=dict(type='str', required=False), tlsa_matching_type=dict(type='int', required=False), tlsa_selector=dict(type='int', required=False), txt_data=dict(type='str', required=False), uri_priority=dict(type='int', required=False), uri_target=dict(type='str', required=False), uri_weight=dict(type='int', required=False), ) ansible_module = IPAAnsibleModule( argument_spec=dict( # general name=dict(type="list", aliases=["record_name"], default=None, required=False), records=dict(type="list", default=None, options=dict( # Here name is a simple string name=dict(type='str', required=True, aliases=['record_name']), **record_spec), ), # general state=dict(type="str", default="present", choices=["present", "absent", "disabled"]), # Add record specific parameters for simple use case **record_spec ), mutually_exclusive=[["name", "records"], ['record_value', 'del_all']], required_one_of=[["name", "records"]], supports_check_mode=True, ) ansible_module._ansible_debug = True return ansible_module def find_dnsrecord(module, dnszone, name): """Find a DNS record based on its name (idnsname).""" _args = { "all": True, "idnsname": to_text(name), } try: _result = module.ipa_command( "dnsrecord_show", to_text(dnszone), _args) except ipalib_errors.NotFound: return None return _result["result"] def check_parameters(module, state, zone_name, record): """Check if parameters are correct.""" if zone_name is None: module.fail_json(msg="Msssing required argument: zone_name") record_type = record.get('record_type', None) record_value = record.get('record_value', None) if record_type is not None: if record_type not in _SUPPORTED_RECORD_TYPES: module.fail_json( msg="Record Type '%s' is not supported." % record_type) # has_record is "True" if the playbook has set any of the full record # attributes (*record or *_rec). has_record = any( (rec in record) or (("%sord" % rec) in record) for rec in _RECORD_FIELDS ) # has_part_record is "True" if the playbook has set any of the # record field attributes. has_part_record = any(record.get(rec, None) for rec in _PART_MAP) # some attributes in the playbook may have a special meaning, # like "ip_address", which is used for either arecord or aaaarecord, # and has_special is true if any of these attributes is set on # on the playbook. special_list = ['ip_address'] has_special = any(record.get(rec, None) for rec in special_list) invalid = [] if state == 'present': if has_record or has_part_record or has_special: if record_value: module.fail_json( msg="Cannot use record data with `record_value`.") elif not record_value: module.fail_json(msg="No record data provided.") invalid = ['del_all'] if state == 'absent': del_all = record.get('del_all', None) if record_value: if has_record or has_part_record or del_all: module.fail_json( msg="Cannot use record data with `record_value`.") elif not (has_record or has_part_record or del_all): module.fail_json( msg="Either a record description or `del_all` is required.") invalid = list(_PART_MAP.keys()) invalid.extend(['create_reverse', 'dns_ttl']) for x in invalid: if x in record: module.fail_json( msg="Variable `%s` cannot be used in state `%s`" % (x, state)) def get_entry_from_module(module, name): """Create an entry dict from attributes in module.""" attrs = [ 'del_all', 'zone_name', 'record_type', 'record_value', 'record_ttl', "ip_address", "create_reverse" ] entry = {'name': name} for key_set in [_RECORD_FIELDS, _PART_MAP, attrs]: entry.update({ key: module.params_get(key) for key in key_set if module.params_get(key) is not None }) return entry def create_reverse_ip_record(module, zone_name, name, ips): """Create a reverse record for an IP (PTR record).""" _cmds = [] for address in ips: reverse_ip = dns.reversename.from_address(address) reverse_zone = dns.resolver.zone_for_name(reverse_ip) reverse_host = to_text(reverse_ip).replace(".%s" % reverse_zone, '') rev_find = find_dnsrecord(module, reverse_zone, reverse_host) if rev_find is None: rev_args = { 'idnsname': to_text(reverse_host), "ptrrecord": "%s.%s" % (name, zone_name) } _cmds.append([reverse_zone, 'dnsrecord_add', rev_args]) return _cmds def ensure_data_is_list(data): """Ensure data is represented as a list.""" return data if isinstance(data, list) else [data] def gen_args(entry): """Generate IPA API arguments for a given `entry`.""" args = {'idnsname': to_text(entry['name'])} if 'del_all' in entry: args['del_all'] = entry['del_all'] record_value = entry.get('record_value', None) if record_value is not None: record_type = entry['record_type'] rec = "{}record".format(record_type.lower()) args[rec] = ensure_data_is_list(record_value) else: for field in _RECORD_FIELDS: record_value = entry.get(field) or entry.get("%sord" % field) if record_value is not None: record_type = field.split('_')[0] rec = "{}record".format(record_type.lower()) args[rec] = ensure_data_is_list(record_value) records = { key: rec for key, rec in _PART_MAP.items() if key in entry } for key, rec in records.items(): args[rec] = entry[key] if 'ip_address' in entry: ip_address = entry['ip_address'] if is_ipv4_addr(ip_address): args['a_part_ip_address'] = ip_address if is_ipv6_addr(ip_address): args['aaaa_part_ip_address'] = ip_address if entry.get('create_reverse', False): if 'a_part_ip_address' in args or 'arecord' in args: args['a_extra_create_reverse'] = True if 'aaaa_part_ip_address' in args or 'aaaarecord' in args: args['aaaa_extra_create_reverse'] = True if 'record_ttl' in entry: args['dnsttl'] = entry['record_ttl'] return args def define_commands_for_present_state(module, zone_name, entry, res_find): """Define commnads for `state: present`.""" _commands = [] name = to_text(entry['name']) args = gen_args(entry) existing = find_dnsrecord(module, zone_name, name) for record, fields in _RECORD_PARTS.items(): part_fields = [f for f in fields if f in args] if part_fields and record in args: record_change_request = True break else: record_change_request = False if res_find is None and not record_change_request: _commands.append([zone_name, 'dnsrecord_add', args]) else: # Create reverse records for existing records for ipv in ['a', 'aaaa']: record = ('%srecord' % ipv) if record in args and ('%s_extra_create_reverse' % ipv) in args: cmds = create_reverse_ip_record( module, zone_name, name, args[record]) _commands.extend(cmds) del args['%s_extra_create_reverse' % ipv] for record, fields in _RECORD_PARTS.items(): part_fields = [f for f in fields if f in args] if part_fields: if record in args: # user wants to update record. if len(args[record]) > 1: module.fail_json(msg="Cannot modify multiple records " "of the same type at once.") mod_record = args[record][0] if existing is None: module.fail_json(msg="`%s` not found." % record) else: # update DNS record _args = {k: args[k] for k in part_fields if k in args} _args["idnsname"] = to_text(args["idnsname"]) _args[record] = mod_record if 'dns_ttl' in args: _args['dns_ttl'] = args['dns_ttl'] _commands.append([zone_name, 'dnsrecord_mod', _args]) # remove record from args, as it will not be used again. del args[record] else: _args = {k: args[k] for k in part_fields if k in args} _args['idnsname'] = name _commands.append([zone_name, 'dnsrecord_add', _args]) # clean used fields from args for f in part_fields: # pylint: disable=invalid-name if f in args: del args[f] else: if record in args: add_list = [] for value in args[record]: if ( res_find is None or record not in res_find or value not in res_find[record] ): add_list.append(value) if add_list: args[record] = add_list _commands.append([zone_name, 'dnsrecord_add', args]) return _commands def define_commands_for_absent_state(module, zone_name, entry, res_find): """Define commands for `state: absent`.""" _commands = [] if res_find is None: return [] args = gen_args(entry) del_all = args.get('del_all', False) records_to_delete = {k: v for k, v in args.items() if k.endswith('record')} if del_all and records_to_delete: module.fail_json(msg="Cannot use del_all and record together.") if not del_all: delete_records = False for record, values in records_to_delete.items(): del_list = [] if record in res_find: for value in values: for rec_found in res_find[record]: if rec_found == value: del_list.append(value) if del_list: args[record] = del_list delete_records = True if delete_records: _commands.append([zone_name, 'dnsrecord_del', args]) else: _commands.append([zone_name, 'dnsrecord_del', args]) return _commands def main(): """Execute DNS record playbook.""" ansible_module = configure_module() global_zone_name = ansible_module.params_get("zone_name") names = ansible_module.params_get("name") records = ansible_module.params_get("records") state = ansible_module.params_get("state") # Check parameters if (names is None or len(names) < 1) and \ (records is None or len(records) < 1): ansible_module.fail_json(msg="One of name and records is required") if state == "present": if names is not None and len(names) != 1: ansible_module.fail_json( msg="Only one record can be added at a time.") if records is not None: names = records # Init changed = False exit_args = {} # Connect to IPA API with ansible_module.ipa_connect(): commands = [] for record in names: if isinstance(record, dict): # ensure name is a string zone_name = record.get("zone_name", global_zone_name) name = record['name'] = str(record['name']) entry = record else: zone_name = global_zone_name name = record entry = get_entry_from_module(ansible_module, name) check_parameters(ansible_module, state, zone_name, entry) res_find = find_dnsrecord(ansible_module, zone_name, name) if state == 'present': cmds = define_commands_for_present_state( ansible_module, zone_name, entry, res_find) elif state == 'absent': cmds = define_commands_for_absent_state( ansible_module, zone_name, entry, res_find) else: ansible_module.fail_json(msg="Unkown state '%s'" % state) if cmds: commands.extend(cmds) # Check mode exit if ansible_module.check_mode: ansible_module.exit_json(changed=len(commands) > 0, **exit_args) # Execute commands for name, command, args in commands: try: result = ansible_module.ipa_command( command, to_text(name), args) if "completed" in result: if result["completed"] > 0: changed = True else: changed = True except ipalib_errors.EmptyModlist: continue except ipalib_errors.DuplicateEntry: continue except Exception as e: error_message = str(e) ansible_module.fail_json( msg="%s: %s: %s" % (command, name, error_message)) # Done ansible_module.exit_json(changed=changed, host=exit_args) if __name__ == "__main__": main()