#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright: Ansible Project # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: dnsimple short_description: Interface with dnsimple.com (a DNS hosting service) description: - "Manages domains and records via the DNSimple API, see the docs: U(http://developer.dnsimple.com/)." options: account_email: description: - Account email. If omitted, the environment variables C(DNSIMPLE_EMAIL) and C(DNSIMPLE_API_TOKEN) will be looked for. - "If those aren't found, a C(.dnsimple) file will be looked for, see: U(https://github.com/mikemaccana/dnsimple-python#getting-started)." - "C(.dnsimple) config files are only supported in dnsimple-python<2.0.0" type: str account_api_token: description: - Account API token. See I(account_email) for more information. type: str domain: description: - Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNSimple. - If omitted, a list of domains will be returned. - If domain is present but the domain doesn't exist, it will be created. type: str record: description: - Record to add, if blank a record for the domain will be created, supports the wildcard (*). type: str record_ids: description: - List of records to ensure they either exist or do not exist. type: list elements: str type: description: - The type of DNS record to create. choices: [ 'A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL', 'CAA' ] type: str ttl: description: - The TTL to give the new record in seconds. default: 3600 type: int value: description: - Record value. - Must be specified when trying to ensure a record exists. type: str priority: description: - Record priority. type: int state: description: - whether the record should exist or not. choices: [ 'present', 'absent' ] default: present type: str solo: description: - Whether the record should be the only one for that record type and record name. - Only use with C(state) is set to C(present) on a record. type: 'bool' default: no sandbox: description: - Use the DNSimple sandbox environment. - Requires a dedicated account in the dnsimple sandbox environment. - Check U(https://developer.dnsimple.com/sandbox/) for more information. type: 'bool' default: no version_added: 3.5.0 requirements: - "dnsimple >= 1.0.0" notes: - "Support for C(dnsimple < 2) is deprecated and will be removed in community.general 5.0.0." author: "Alex Coomans (@drcapulet)" ''' EXAMPLES = ''' - name: Authenticate using email and API token and fetch all domains community.general.dnsimple: account_email: test@example.com account_api_token: dummyapitoken delegate_to: localhost - name: Delete a domain community.general.dnsimple: domain: my.com state: absent delegate_to: localhost - name: Create a test.my.com A record to point to 127.0.0.1 community.general.dnsimple: domain: my.com record: test type: A value: 127.0.0.1 delegate_to: localhost register: record - name: Delete record using record_ids community.general.dnsimple: domain: my.com record_ids: '{{ record["id"] }}' state: absent delegate_to: localhost - name: Create a my.com CNAME record to example.com community.general.dnsimple: domain: my.com record: '' type: CNAME value: example.com state: present delegate_to: localhost - name: Change TTL value for a record community.general.dnsimple: domain: my.com record: '' type: CNAME value: example.com ttl: 600 state: present delegate_to: localhost - name: Delete the record community.general.dnsimple: domain: my.com record: '' type: CNAME value: example.com state: absent delegate_to: localhost ''' RETURN = r"""# """ import traceback import re from ansible_collections.community.general.plugins.module_utils.version import LooseVersion class DNSimpleV1(): """class which uses dnsimple-python < 2""" def __init__(self, account_email, account_api_token, sandbox, module): """init""" self.module = module self.account_email = account_email self.account_api_token = account_api_token self.sandbox = sandbox self.dnsimple_client() def dnsimple_client(self): """creates a dnsimple client object""" if self.account_email and self.account_api_token: self.client = DNSimple(sandbox=self.sandbox, email=self.account_email, api_token=self.account_api_token) else: self.client = DNSimple(sandbox=self.sandbox) def get_all_domains(self): """returns a list of all domains""" domain_list = self.client.domains() return [d['domain'] for d in domain_list] def get_domain(self, domain): """returns a single domain by name or id""" try: dr = self.client.domain(domain)['domain'] except DNSimpleException as e: exception_string = str(e.args[0]['message']) if re.match(r"^Domain .+ not found$", exception_string): dr = None else: raise return dr def create_domain(self, domain): """create a single domain""" return self.client.add_domain(domain)['domain'] def delete_domain(self, domain): """delete a single domain""" self.client.delete(domain) def get_records(self, domain, dnsimple_filter=None): """return dns ressource records which match a specified filter""" return [r['record'] for r in self.client.records(str(domain), params=dnsimple_filter)] def delete_record(self, domain, rid): """delete a single dns ressource record""" self.client.delete_record(str(domain), rid) def update_record(self, domain, rid, ttl=None, priority=None): """update a single dns ressource record""" data = {} if ttl: data['ttl'] = ttl if priority: data['priority'] = priority return self.client.update_record(str(domain), str(rid), data)['record'] def create_record(self, domain, name, record_type, content, ttl=None, priority=None): """create a single dns ressource record""" data = { 'name': name, 'type': record_type, 'content': content, } if ttl: data['ttl'] = ttl if priority: data['priority'] = priority return self.client.add_record(str(domain), data)['record'] class DNSimpleV2(): """class which uses dnsimple-python >= 2""" def __init__(self, account_email, account_api_token, sandbox, module): """init""" self.module = module self.account_email = account_email self.account_api_token = account_api_token self.sandbox = sandbox self.pagination_per_page = 30 self.dnsimple_client() self.dnsimple_account() def dnsimple_client(self): """creates a dnsimple client object""" if self.account_email and self.account_api_token: client = Client(sandbox=self.sandbox, email=self.account_email, access_token=self.account_api_token) else: msg = "Option account_email or account_api_token not provided. " \ "Dnsimple authentiction with a .dnsimple config file is not " \ "supported with dnsimple-python>=2.0.0" raise DNSimpleException(msg) client.identity.whoami() self.client = client def dnsimple_account(self): """select a dnsimple account. If a user token is used for authentication, this user must only have access to a single account""" account = self.client.identity.whoami().data.account # user supplied a user token instead of account api token if not account: accounts = Accounts(self.client).list_accounts().data if len(accounts) != 1: msg = "The provided dnsimple token is a user token with multiple accounts." \ "Use an account token or a user token with access to a single account." \ "See https://support.dnsimple.com/articles/api-access-token/" raise DNSimpleException(msg) account = accounts[0] self.account = account def get_all_domains(self): """returns a list of all domains""" domain_list = self._get_paginated_result(self.client.domains.list_domains, account_id=self.account.id) return [d.__dict__ for d in domain_list] def get_domain(self, domain): """returns a single domain by name or id""" try: dr = self.client.domains.get_domain(self.account.id, domain).data.__dict__ except DNSimpleException as e: exception_string = str(e.message) if re.match(r"^Domain .+ not found$", exception_string): dr = None else: raise return dr def create_domain(self, domain): """create a single domain""" return self.client.domains.create_domain(self.account.id, domain).data.__dict__ def delete_domain(self, domain): """delete a single domain""" self.client.domains.delete_domain(self.account.id, domain) def get_records(self, zone, dnsimple_filter=None): """return dns ressource records which match a specified filter""" records_list = self._get_paginated_result(self.client.zones.list_records, account_id=self.account.id, zone=zone, filter=dnsimple_filter) return [d.__dict__ for d in records_list] def delete_record(self, domain, rid): """delete a single dns ressource record""" self.client.zones.delete_record(self.account.id, domain, rid) def update_record(self, domain, rid, ttl=None, priority=None): """update a single dns ressource record""" zr = ZoneRecordUpdateInput(ttl=ttl, priority=priority) result = self.client.zones.update_record(self.account.id, str(domain), str(rid), zr).data.__dict__ return result def create_record(self, domain, name, record_type, content, ttl=None, priority=None): """create a single dns ressource record""" zr = ZoneRecordInput(name=name, type=record_type, content=content, ttl=ttl, priority=priority) return self.client.zones.create_record(self.account.id, str(domain), zr).data.__dict__ def _get_paginated_result(self, operation, **options): """return all results of a paginated api response""" records_pagination = operation(per_page=self.pagination_per_page, **options).pagination result_list = [] for page in range(1, records_pagination.total_pages + 1): page_data = operation(per_page=self.pagination_per_page, page=page, **options).data result_list.extend(page_data) return result_list DNSIMPLE_IMP_ERR = [] HAS_DNSIMPLE = False try: # try to import dnsimple >= 2.0.0 from dnsimple import Client, DNSimpleException from dnsimple.service import Accounts from dnsimple.version import version as dnsimple_version from dnsimple.struct.zone_record import ZoneRecordUpdateInput, ZoneRecordInput HAS_DNSIMPLE = True except ImportError: DNSIMPLE_IMP_ERR.append(traceback.format_exc()) if not HAS_DNSIMPLE: # try to import dnsimple < 2.0.0 try: from dnsimple.dnsimple import __version__ as dnsimple_version from dnsimple import DNSimple from dnsimple.dnsimple import DNSimpleException HAS_DNSIMPLE = True except ImportError: DNSIMPLE_IMP_ERR.append(traceback.format_exc()) from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback def main(): module = AnsibleModule( argument_spec=dict( account_email=dict(type='str', fallback=(env_fallback, ['DNSIMPLE_EMAIL'])), account_api_token=dict(type='str', no_log=True, fallback=(env_fallback, ['DNSIMPLE_API_TOKEN'])), domain=dict(type='str'), record=dict(type='str'), record_ids=dict(type='list', elements='str'), type=dict(type='str', choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL', 'CAA']), ttl=dict(type='int', default=3600), value=dict(type='str'), priority=dict(type='int'), state=dict(type='str', choices=['present', 'absent'], default='present'), solo=dict(type='bool', default=False), sandbox=dict(type='bool', default=False), ), required_together=[ ['record', 'value'] ], supports_check_mode=True, ) if not HAS_DNSIMPLE: module.fail_json(msg=missing_required_lib('dnsimple'), exception=DNSIMPLE_IMP_ERR[0]) account_email = module.params.get('account_email') account_api_token = module.params.get('account_api_token') domain = module.params.get('domain') record = module.params.get('record') record_ids = module.params.get('record_ids') record_type = module.params.get('type') ttl = module.params.get('ttl') value = module.params.get('value') priority = module.params.get('priority') state = module.params.get('state') is_solo = module.params.get('solo') sandbox = module.params.get('sandbox') DNSIMPLE_MAJOR_VERSION = LooseVersion(dnsimple_version).version[0] try: if DNSIMPLE_MAJOR_VERSION > 1: ds = DNSimpleV2(account_email, account_api_token, sandbox, module) else: module.deprecate( 'Support for python-dnsimple < 2 is deprecated. ' 'Update python-dnsimple to version >= 2.0.0', version='5.0.0', collection_name='community.general' ) ds = DNSimpleV1(account_email, account_api_token, sandbox, module) # Let's figure out what operation we want to do # No domain, return a list if not domain: all_domains = ds.get_all_domains() module.exit_json(changed=False, result=all_domains) # Domain & No record if record is None and not record_ids: if domain.isdigit(): typed_domain = int(domain) else: typed_domain = str(domain) dr = ds.get_domain(typed_domain) # domain does not exist if state == 'present': if dr: module.exit_json(changed=False, result=dr) else: if module.check_mode: module.exit_json(changed=True) else: response = ds.create_domain(domain) module.exit_json(changed=True, result=response) # state is absent else: if dr: if not module.check_mode: ds.delete_domain(domain) module.exit_json(changed=True) else: module.exit_json(changed=False) # need the not none check since record could be an empty string if record is not None: if not record_type: module.fail_json(msg="Missing the record type") if not value: module.fail_json(msg="Missing the record value") records_list = ds.get_records(domain, dnsimple_filter={'name': record}) rr = next((r for r in records_list if r['name'] == record and r['type'] == record_type and r['content'] == value), None) if state == 'present': changed = False if is_solo: # delete any records that have the same name and record type same_type = [r['id'] for r in records_list if r['name'] == record and r['type'] == record_type] if rr: same_type = [rid for rid in same_type if rid != rr['id']] if same_type: if not module.check_mode: for rid in same_type: ds.delete_record(domain, rid) changed = True if rr: # check if we need to update if rr['ttl'] != ttl or rr['priority'] != priority: if module.check_mode: module.exit_json(changed=True) else: response = ds.update_record(domain, rr['id'], ttl, priority) module.exit_json(changed=True, result=response) else: module.exit_json(changed=changed, result=rr) else: # create it if module.check_mode: module.exit_json(changed=True) else: response = ds.create_record(domain, record, record_type, value, ttl, priority) module.exit_json(changed=True, result=response) # state is absent else: if rr: if not module.check_mode: ds.delete_record(domain, rr['id']) module.exit_json(changed=True) else: module.exit_json(changed=False) # Make sure these record_ids either all exist or none if record_ids: current_records = ds.get_records(domain, dnsimple_filter=None) current_record_ids = [str(d['id']) for d in current_records] wanted_record_ids = [str(r) for r in record_ids] if state == 'present': difference = list(set(wanted_record_ids) - set(current_record_ids)) if difference: module.fail_json(msg="Missing the following records: %s" % difference) else: module.exit_json(changed=False) # state is absent else: difference = list(set(wanted_record_ids) & set(current_record_ids)) if difference: if not module.check_mode: for rid in difference: ds.delete_record(domain, rid) module.exit_json(changed=True) else: module.exit_json(changed=False) except DNSimpleException as e: if DNSIMPLE_MAJOR_VERSION > 1: module.fail_json(msg="DNSimple exception: %s" % e.message) else: module.fail_json(msg="DNSimple exception: %s" % str(e.args[0]['message'])) module.fail_json(msg="Unknown what you wanted me to do") if __name__ == '__main__': main()