diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/dnsimple.py')
-rw-r--r-- | ansible_collections/community/general/plugins/modules/dnsimple.py | 434 |
1 files changed, 434 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/dnsimple.py b/ansible_collections/community/general/plugins/modules/dnsimple.py new file mode 100644 index 000000000..df41f73a6 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/dnsimple.py @@ -0,0 +1,434 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright Ansible Project +# +# 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 + +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/)." +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +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: false + 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: false + version_added: 3.5.0 +requirements: + - "dnsimple >= 2.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 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, user_agent="ansible/community.general") + 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()) + +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 < 2: + module.fail_json( + msg='Support for python-dnsimple < 2 has been removed in community.general 5.0.0. Update python-dnsimple to version >= 2.0.0.') + ds = DNSimpleV2(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() |