From 2547932e3d99b7ce667e1da775403acccc33ecaa Mon Sep 17 00:00:00 2001 From: Edward Hilgendorf Date: Sat, 11 Dec 2021 12:14:32 -0800 Subject: [PATCH] add dnsimple_info module, see issue #3569 (#3739) * add dnsimple_info module, see issue #3569 https://github.com/ansible-collections/community.general/issues/3569#issuecomment-945002861 * Update plugins/modules/net_tools/dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update BOTMETA.yml Update dnsimple_info.py Create dnsimple_info.py Create dnsimple_info.py pep8 Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update dnsimple_info.py add returns pep8 spacing Update dnsimple_info.py Update dnsimple_info.py change return results to list fix time stamps Update dnsimple_info.py remove extra comma Update plugins/modules/net_tools/dnsimple_info.py Update test_dnsimple_info.py Update dnsimple_info.py fix descriptions Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py missing punctuation throughout docs Update dnsimple_info.py add elements in descriptions Update dnsimple_info.py indentation error Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py Update dnsimple_info.py refactor, remove unneeded arguments refactor and error handling formatting add unit test Update test_dnsimple_info.py Update test_dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update plugins/modules/net_tools/dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py assert fail/exit Update test_dnsimple_info.py pep8 fixes Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Update test_dnsimple_info.py Co-Authored-By: Felix Fontein Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 2 + plugins/modules/dnsimple_info.py | 1 + plugins/modules/net_tools/dnsimple_info.py | 335 ++++++++++++++++++ .../modules/net_tools/test_dnsimple_info.py | 113 ++++++ 4 files changed, 451 insertions(+) create mode 120000 plugins/modules/dnsimple_info.py create mode 100644 plugins/modules/net_tools/dnsimple_info.py create mode 100644 tests/unit/plugins/modules/net_tools/test_dnsimple_info.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 4ae85b214d..02f6408696 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -619,6 +619,8 @@ files: labels: cloudflare_dns $modules/net_tools/dnsimple.py: maintainers: drcapulet + $modules/net_tools/dnsimple_info.py: + maintainers: edhilgendorf $modules/net_tools/dnsmadeeasy.py: maintainers: briceburg $modules/net_tools/gandi_livedns.py: diff --git a/plugins/modules/dnsimple_info.py b/plugins/modules/dnsimple_info.py new file mode 120000 index 0000000000..853fbaa533 --- /dev/null +++ b/plugins/modules/dnsimple_info.py @@ -0,0 +1 @@ +./net_tools/dnsimple_info.py \ No newline at end of file diff --git a/plugins/modules/net_tools/dnsimple_info.py b/plugins/modules/net_tools/dnsimple_info.py new file mode 100644 index 0000000000..4ac22be0cb --- /dev/null +++ b/plugins/modules/net_tools/dnsimple_info.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Edward Hilgendorf, +# 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 = r''' +--- +module: dnsimple_info + +short_description: Pull basic info from DNSimple API + +version_added: "4.2.0" + +description: Retrieve existing records and domains from DNSimple API. + +options: + name: + description: + - The domain name to retrieve info from. + - Will return all associated records for this domain if specified. + - If not specified, will return all domains associated with the account ID. + type: str + + account_id: + description: The account ID to query. + required: true + type: str + + api_key: + description: The API key to use. + required: true + type: str + + record: + description: + - The record to find. + - If specified, only this record will be returned instead of all records. + required: false + type: str + + sandbox: + description: Whether or not to use sandbox environment. + required: false + default: false + type: bool + +author: + - Edward Hilgendorf (@edhilgendorf) +''' + +EXAMPLES = r''' +- name: Get all domains from an account + community.general.dnsimple_info: + account_id: "1234" + api_key: "1234" + +- name: Get all records from a domain + community.general.dnsimple_info: + name: "example.com" + account_id: "1234" + api_key: "1234" + +- name: Get all info from a matching record + community.general.dnsimple_info: + name: "example.com" + record: "subdomain" + account_id: "1234" + api_key: "1234" +''' + +RETURN = r''' +dnsimple_domain_info: + description: Returns a list of dictionaries of all domains associated with the supplied account ID. + type: list + elements: dict + returned: success when I(name) is not specified + sample: + - account_id: 1234 + created_at: '2021-10-16T21:25:42Z' + id: 123456 + last_transferred_at: + name: example.com + reverse: false + secondary: false + updated_at: '2021-11-10T20:22:50Z' + contains: + account_id: + description: The account ID. + type: int + created_at: + description: When the domain entry was created. + type: str + id: + description: ID of the entry. + type: int + last_transferred_at: + description: Date the domain was transferred, or empty if not. + type: str + name: + description: Name of the record. + type: str + reverse: + description: Whether or not it is a reverse zone record. + type: bool + updated_at: + description: When the domain entry was updated. + type: str + +dnsimple_records_info: + description: Returns a list of dictionaries with all records for the domain supplied. + type: list + elements: dict + returned: success when I(name) is specified, but I(record) is not + sample: + - content: ns1.dnsimple.com admin.dnsimple.com + created_at: '2021-10-16T19:07:34Z' + id: 12345 + name: 'catheadbiscuit' + parent_id: null + priority: null + regions: + - global + system_record: true + ttl: 3600 + type: SOA + updated_at: '2021-11-15T23:55:51Z' + zone_id: example.com + contains: + content: + description: Content of the returned record. + type: str + created_at: + description: When the domain entry was created. + type: str + id: + description: ID of the entry. + type: int + name: + description: Name of the record. + type: str + parent_id: + description: Parent record or null. + type: int + priority: + description: Priority setting of the record. + type: str + regions: + description: List of regions where the record is available. + type: list + system_record: + description: Whether or not it is a system record. + type: bool + ttl: + description: Record TTL. + type: int + type: + description: Record type. + type: str + updated_at: + description: When the domain entry was updated. + type: str + zone_id: + description: ID of the zone that the record is associated with. + type: str +dnsimple_record_info: + description: Returns a list of dictionaries that match the record supplied. + returned: success when I(name) and I(record) are specified + type: list + elements: dict + sample: + - content: 1.2.3.4 + created_at: '2021-11-15T23:55:51Z' + id: 123456 + name: catheadbiscuit + parent_id: null + priority: null + regions: + - global + system_record: false + ttl: 3600 + type: A + updated_at: '2021-11-15T23:55:51Z' + zone_id: example.com + contains: + content: + description: Content of the returned record. + type: str + created_at: + description: When the domain entry was created. + type: str + id: + description: ID of the entry. + type: int + name: + description: Name of the record. + type: str + parent_id: + description: Parent record or null. + type: int + priority: + description: Priority setting of the record. + type: str + regions: + description: List of regions where the record is available. + type: list + system_record: + description: Whether or not it is a system record. + type: bool + ttl: + description: Record TTL. + type: int + type: + description: Record type. + type: str + updated_at: + description: When the domain entry was updated. + type: str + zone_id: + description: ID of the zone that the record is associated with. + type: str +''' + +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib +import json + +try: + from requests import Request, Session +except ImportError: + HAS_ANOTHER_LIBRARY = False + ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_ANOTHER_LIBRARY = True + + +def build_url(account, key, is_sandbox): + headers = {'Accept': 'application/json', + 'Authorization': 'Bearer ' + key} + url = 'https://api{sandbox}.dnsimple.com/'.format( + sandbox=".sandbox" if is_sandbox else "") + 'v2/' + account + req = Request(url=url, headers=headers) + prepped_request = req.prepare() + return prepped_request + + +def iterate_data(module, request_object): + base_url = request_object.url + response = Session().send(request_object) + if 'pagination' in response.json(): + data = response.json()["data"] + pages = response.json()["pagination"]["total_pages"] + if int(pages) > 1: + for page in range(1, pages): + page = page + 1 + request_object.url = base_url + '&page=' + str(page) + new_results = Session().send(request_object) + data = data + new_results.json()["data"] + return(data) + else: + module.fail_json('API Call failed, check ID, key and sandbox values') + + +def record_info(dnsimple_mod, req_obj): + req_obj.url, req_obj.method = req_obj.url + '/zones/' + dnsimple_mod.params["name"] + '/records?name=' + dnsimple_mod.params["record"], 'GET' + return iterate_data(dnsimple_mod, req_obj) + + +def domain_info(dnsimple_mod, req_obj): + req_obj.url, req_obj.method = req_obj.url + '/zones/' + dnsimple_mod.params["name"] + '/records?per_page=100', 'GET' + return iterate_data(dnsimple_mod, req_obj) + + +def account_info(dnsimple_mod, req_obj): + req_obj.url, req_obj.method = req_obj.url + '/zones/?per_page=100', 'GET' + return iterate_data(dnsimple_mod, req_obj) + + +def main(): + # define available arguments/parameters a user can pass to the module + fields = { + "account_id": {"required": True, "type": "str"}, + "api_key": {"required": True, "type": "str", "no_log": True}, + "name": {"required": False, "type": "str"}, + "record": {"required": False, "type": "str"}, + "sandbox": {"required": False, "type": "bool", "default": False} + } + + result = { + 'changed': False + } + + module = AnsibleModule( + argument_spec=fields, + supports_check_mode=True + ) + + params = module.params + req = build_url(params['account_id'], + params['api_key'], + params['sandbox']) + + if not HAS_ANOTHER_LIBRARY: + # Needs: from ansible.module_utils.basic import missing_required_lib + module.exit_json( + msg=missing_required_lib('another_library'), + exception=ANOTHER_LIBRARY_IMPORT_ERROR) + + # At minimum we need account and key + if params['account_id'] and params['api_key']: + # If we have a record return info on that record + if params['name'] and params['record']: + result['dnsimple_record_info'] = record_info(module, req) + module.exit_json(**result) + + # If we have the account only and domain, return records for the domain + elif params['name']: + result['dnsimple_records_info'] = domain_info(module, req) + module.exit_json(**result) + + # If we have the account only, return domains + else: + result['dnsimple_domain_info'] = account_info(module, req) + module.exit_json(**result) + else: + module.fail_json(msg="Need at least account_id and api_key") + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/net_tools/test_dnsimple_info.py b/tests/unit/plugins/modules/net_tools/test_dnsimple_info.py new file mode 100644 index 0000000000..158f38f352 --- /dev/null +++ b/tests/unit/plugins/modules/net_tools/test_dnsimple_info.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# 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 + +from ansible_collections.community.general.plugins.modules.net_tools import dnsimple_info +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleFailJson, ModuleTestCase, set_module_args, AnsibleExitJson +from httmock import response +from httmock import with_httmock +from httmock import urlmatch +import pytest + + +dnsimple = pytest.importorskip('dnsimple_info') + + +@urlmatch(netloc='(.)*dnsimple.com(.)*', + path='/v2/[0-9]*/zones/') +def zones_resp(url, request): + """return domains""" + headers = {'content-type': 'application/json'} + data_content = {"data": + [{"account_id": "1234", }, ], + "pagination": {"total_pages": 1}} + content = data_content + return response(200, content, headers, None, 5, request) + + +@urlmatch(netloc='(.)*dnsimple.com(.)*', + path='/v2/[0-9]*/zones/(.)*/records(.*)') +def records_resp(url, request): + """return record(s)""" + headers = {'content-type': 'application/json'} + data_content = {"data": + [{"content": "example", + "name": "example.com"}], + "pagination": {"total_pages": 1}} + content = data_content + return response(200, content, headers, None, 5, request) + + +class TestDNSimple_Info(ModuleTestCase): + """Main class for testing dnsimple module.""" + + def setUp(self): + + """Setup.""" + super(TestDNSimple_Info, self).setUp() + self.module = dnsimple_info + + def tearDown(self): + """Teardown.""" + super(TestDNSimple_Info, self).tearDown() + + def test_with_no_parameters(self): + """Failure must occurs when all parameters are missing""" + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + self.module.main() + + @with_httmock(zones_resp) + def test_only_key_and_account(self): + """key and account will pass, returns domains""" + account_id = "1234" + with self.assertRaises(AnsibleExitJson) as exc_info: + set_module_args({ + "api_key": "abcd1324", + "account_id": account_id + }) + self.module.main() + result = exc_info.exception.args[0] + # nothing should change + self.assertFalse(result['changed']) + # we should return at least one item with the matching account ID + assert result['dnsimple_domain_info'][0]["account_id"] == account_id + + @with_httmock(records_resp) + def test_only_name_without_record(self): + """name and no record should not fail, returns the record""" + name = "example.com" + with self.assertRaises(AnsibleExitJson) as exc_info: + set_module_args({ + "api_key": "abcd1324", + "name": "example.com", + "account_id": "1234" + }) + self.module.main() + result = exc_info.exception.args[0] + # nothing should change + self.assertFalse(result['changed']) + # we should return at least one item with mathing domain + assert result['dnsimple_records_info'][0]['name'] == name + + @with_httmock(records_resp) + def test_name_and_record(self): + """name and record should not fail, returns the record""" + record = "example" + with self.assertRaises(AnsibleExitJson) as exc_info: + set_module_args({ + "api_key": "abcd1324", + "account_id": "1234", + "name": "example.com", + "record": "example" + }) + self.module.main() + result = exc_info.exception.args[0] + # nothing should change + self.assertFalse(result['changed']) + # we should return at least one item and content should match + assert result['dnsimple_record_info'][0]['content'] == record