1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Add gandi_livedns module (#328)

* Add gandi_livedns module

This module uses REST API to register, update and delete domain name
entries in Gandi DNS service (https://www.gandi.net/en/domain).

* Apply suggestions from code review

* Update plugins/module_utils/gandi_livedns_api.py

Co-authored-by: Gregory Thiemonge <greg@thiemonge.org>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Gregory Thiemonge 2021-03-21 11:25:24 +01:00 committed by GitHub
parent 606eb0df15
commit 81f3ad45c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 652 additions and 0 deletions

View file

@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2019 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.urls import fetch_url
class GandiLiveDNSAPI(object):
api_endpoint = 'https://api.gandi.net/v5/livedns'
changed = False
error_strings = {
400: 'Bad request',
401: 'Permission denied',
404: 'Resource not found',
}
attribute_map = {
'record': 'rrset_name',
'type': 'rrset_type',
'ttl': 'rrset_ttl',
'values': 'rrset_values'
}
def __init__(self, module):
self.module = module
self.api_key = module.params['api_key']
def _build_error_message(self, module, info):
s = ''
body = info.get('body')
if body:
errors = module.from_json(body).get('errors')
if errors:
error = errors[0]
name = error.get('name')
if name:
s += '{0} :'.format(name)
description = error.get('description')
if description:
s += description
return s
def _gandi_api_call(self, api_call, method='GET', payload=None, error_on_404=True):
headers = {'Authorization': 'Apikey {0}'.format(self.api_key),
'Content-Type': 'application/json'}
data = None
if payload:
try:
data = json.dumps(payload)
except Exception as e:
self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(e))
resp, info = fetch_url(self.module,
self.api_endpoint + api_call,
headers=headers,
data=data,
method=method)
error_msg = ''
if info['status'] >= 400 and (info['status'] != 404 or error_on_404):
err_s = self.error_strings.get(info['status'], '')
error_msg = "API Error {0}: {1}".format(err_s, self._build_error_message(self.module, info))
result = None
try:
content = resp.read()
except AttributeError:
content = None
if content:
try:
result = json.loads(to_text(content, errors='surrogate_or_strict'))
except (getattr(json, 'JSONDecodeError', ValueError)) as e:
error_msg += "; Failed to parse API response with error {0}: {1}".format(to_native(e), content)
if error_msg:
self.module.fail_json(msg=error_msg)
return result, info['status']
def build_result(self, result, domain):
if result is None:
return None
res = {}
for k in self.attribute_map:
v = result.get(self.attribute_map[k], None)
if v is not None:
if k == 'record' and v == '@':
v = ''
res[k] = v
res['domain'] = domain
return res
def build_results(self, results, domain):
if results is None:
return []
return [self.build_result(r, domain) for r in results]
def get_records(self, record, type, domain):
url = '/domains/%s/records' % (domain)
if record:
url += '/%s' % (record)
if type:
url += '/%s' % (type)
records, status = self._gandi_api_call(url, error_on_404=False)
if status == 404:
return []
if not isinstance(records, list):
records = [records]
# filter by type if record is not set
if not record and type:
records = [r
for r in records
if r['rrset_type'] == type]
return records
def create_record(self, record, type, values, ttl, domain):
url = '/domains/%s/records' % (domain)
new_record = {
'rrset_name': record,
'rrset_type': type,
'rrset_values': values,
'rrset_ttl': ttl,
}
record, status = self._gandi_api_call(url, method='POST', payload=new_record)
if status in (200, 201,):
return new_record
return None
def update_record(self, record, type, values, ttl, domain):
url = '/domains/%s/records/%s/%s' % (domain, record, type)
new_record = {
'rrset_values': values,
'rrset_ttl': ttl,
}
record = self._gandi_api_call(url, method='PUT', payload=new_record)[0]
return record
def delete_record(self, record, type, domain):
url = '/domains/%s/records/%s/%s' % (domain, record, type)
self._gandi_api_call(url, method='DELETE')
def delete_dns_record(self, record, type, values, domain):
if record == '':
record = '@'
records = self.get_records(record, type, domain)
if records:
cur_record = records[0]
self.changed = True
if values is not None and set(cur_record['rrset_values']) != set(values):
new_values = set(cur_record['rrset_values']) - set(values)
if new_values:
# Removing one or more values from a record, we update the record with the remaining values
self.update_record(record, type, list(new_values), cur_record['rrset_ttl'], domain)
records = self.get_records(record, type, domain)
return records[0], self.changed
if not self.module.check_mode:
self.delete_record(record, type, domain)
else:
cur_record = None
return None, self.changed
def ensure_dns_record(self, record, type, ttl, values, domain):
if record == '':
record = '@'
records = self.get_records(record, type, domain)
if records:
cur_record = records[0]
do_update = False
if ttl is not None and cur_record['rrset_ttl'] != ttl:
do_update = True
if values is not None and set(cur_record['rrset_values']) != set(values):
do_update = True
if do_update:
if self.module.check_mode:
result = dict(
rrset_type=type,
rrset_name=record,
rrset_values=values,
rrset_ttl=ttl
)
else:
self.update_record(record, type, values, ttl, domain)
records = self.get_records(record, type, domain)
result = records[0]
self.changed = True
return result, self.changed
else:
return cur_record, self.changed
if self.module.check_mode:
new_record = dict(
rrset_type=type,
rrset_name=record,
rrset_values=values,
rrset_ttl=ttl
)
result = new_record
else:
result = self.create_record(record, type, values, ttl, domain)
self.changed = True
return result, self.changed

View file

@ -0,0 +1 @@
net_tools/gandi_livedns.py

View file

@ -0,0 +1,187 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# 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: gandi_livedns
author:
- Gregory Thiemonge (@gthiemonge)
version_added: "2.3.0"
short_description: Manage Gandi LiveDNS records
description:
- "Manages DNS records by the Gandi LiveDNS API, see the docs: U(https://doc.livedns.gandi.net/)."
options:
api_key:
description:
- Account API token.
type: str
required: true
record:
description:
- Record to add.
type: str
required: true
state:
description:
- Whether the record(s) should exist or not.
type: str
choices: [ absent, present ]
default: present
ttl:
description:
- The TTL to give the new record.
- Required when I(state=present).
type: int
type:
description:
- The type of DNS record to create.
type: str
required: true
values:
description:
- The record values.
- Required when I(state=present).
type: list
elements: str
domain:
description:
- The name of the Domain to work with (for example, "example.com").
required: true
type: str
notes:
- Supports C(check_mode).
'''
EXAMPLES = r'''
- name: Create a test A record to point to 127.0.0.1 in the my.com domain
community.general.gandi_livedns:
domain: my.com
record: test
type: A
values:
- 127.0.0.1
ttl: 7200
api_key: dummyapitoken
register: record
- name: Create a mail CNAME record to www.my.com domain
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
values:
- www
ttl: 7200
api_key: dummyapitoken
state: present
- name: Change its TTL
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
values:
- www
ttl: 10800
api_key: dummyapitoken
state: present
- name: Delete the record
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
api_key: dummyapitoken
state: absent
'''
RETURN = r'''
record:
description: A dictionary containing the record data.
returned: success, except on record deletion
type: dict
contains:
values:
description: The record content (details depend on record type).
returned: success
type: list
elements: str
sample:
- 192.0.2.91
- 192.0.2.92
record:
description: The record name.
returned: success
type: str
sample: www
ttl:
description: The time-to-live for the record.
returned: success
type: int
sample: 300
type:
description: The record type.
returned: success
type: str
sample: A
domain:
description: The domain associated with the record.
returned: success
type: str
sample: my.com
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.gandi_livedns_api import GandiLiveDNSAPI
def main():
module = AnsibleModule(
argument_spec=dict(
api_key=dict(type='str', required=True, no_log=True),
record=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
ttl=dict(type='int'),
type=dict(type='str', required=True),
values=dict(type='list', elements='str'),
domain=dict(type='str', required=True),
),
supports_check_mode=True,
required_if=[
('state', 'present', ['values', 'ttl']),
],
)
gandi_api = GandiLiveDNSAPI(module)
if module.params['state'] == 'present':
ret, changed = gandi_api.ensure_dns_record(module.params['record'],
module.params['type'],
module.params['ttl'],
module.params['values'],
module.params['domain'])
else:
ret, changed = gandi_api.delete_dns_record(module.params['record'],
module.params['type'],
module.params['values'],
module.params['domain'])
result = dict(
changed=changed,
)
if ret:
result['record'] = gandi_api.build_result(ret,
module.params['domain'])
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
cloud/gandi
unsupported

View file

@ -0,0 +1,34 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
gandi_livedns_domain_name: "ansible-tests.org"
gandi_livedns_record_items:
# Single A record
- record: test-www
type: A
values:
- 10.10.10.10
ttl: 400
update_values:
- 10.10.10.11
update_ttl: 800
# Multiple A records
- record: test-www-multiple
type: A
ttl: 3600
values:
- 10.10.11.10
- 10.10.11.10
update_values:
- 10.10.11.11
- 10.10.11.13
# CNAME
- record: test-cname
type: CNAME
ttl: 10800
values:
- test-www2
update_values:
- test-www

View file

@ -0,0 +1,67 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: test absent dns record
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
type: "{{ item.type }}"
ttl: "{{ item.ttl }}"
state: absent
register: result
- name: verify test absent dns record
assert:
that:
- result is successful
- name: test create a dns record in check mode
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item['values'] }}"
ttl: "{{ item.ttl }}"
type: "{{ item.type }}"
check_mode: yes
register: result
- name: verify test create a dns record in check mode
assert:
that:
- result is changed
- name: test create a dns record
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item['values'] }}"
ttl: "{{ item.ttl }}"
type: "{{ item.type }}"
register: result
- name: verify test create a dns record
assert:
that:
- result is changed
- result.record['values'] == {{ item['values'] }}
- result.record.record == "{{ item.record }}"
- result.record.type == "{{ item.type }}"
- result.record.ttl == {{ item.ttl }}
- name: test create a dns record idempotence
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item['values'] }}"
ttl: "{{ item.ttl }}"
type: "{{ item.type }}"
register: result
- name: verify test create a dns record idempotence
assert:
that:
- result is not changed
- result.record['values'] == {{ item['values'] }}
- result.record.record == "{{ item.record }}"
- result.record.type == "{{ item.type }}"
- result.record.ttl == {{ item.ttl }}

View file

@ -0,0 +1,5 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- include_tasks: record.yml
with_items: "{{ gandi_livedns_record_items }}"

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- include_tasks: create_record.yml
- include_tasks: update_record.yml
- include_tasks: remove_record.yml

View file

@ -0,0 +1,59 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: test remove a dns record in check mode
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
type: "{{ item.type }}"
state: absent
check_mode: yes
register: result
- name: verify test remove a dns record in check mode
assert:
that:
- result is changed
- name: test remove a dns record
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
type: "{{ item.type }}"
state: absent
register: result
- name: verify test remove a dns record
assert:
that:
- result is changed
- name: test remove a dns record idempotence
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
type: "{{ item.type }}"
state: absent
register: result
- name: verify test remove a dns record idempotence
assert:
that:
- result is not changed
- name: test remove second dns record idempotence
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item['values'] }}"
type: "{{ item.type }}"
state: absent
register: result
- name: verify test remove a dns record idempotence
assert:
that:
- result is not changed

View file

@ -0,0 +1,57 @@
# Copyright: (c) 2020 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: test update or add another dns record in check mode
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
ttl: "{{ item.update_ttl | default(item.ttl) }}"
type: "{{ item.type }}"
check_mode: yes
register: result
- name: verify test update in check mode
assert:
that:
- result is changed
- result.record['values'] == {{ item.update_values | default(item['values']) }}
- result.record.record == "{{ item.record }}"
- result.record.type == "{{ item.type }}"
- result.record.ttl == {{ item.update_ttl | default(item.ttl) }}
- name: test update or add another dns record
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
ttl: "{{ item.update_ttl | default(item.ttl) }}"
type: "{{ item.type }}"
register: result
- name: verify test update a dns record
assert:
that:
- result is changed
- result.record['values'] == {{ item.update_values | default(item['values']) }}
- result.record.record == "{{ item.record }}"
- result.record.ttl == {{ item.update_ttl | default(item.ttl) }}
- result.record.type == "{{ item.type }}"
- name: test update or add another dns record idempotence
community.general.gandi_livedns:
api_key: "{{ gandi_api_key }}"
record: "{{ item.record }}"
domain: "{{ gandi_livedns_domain_name }}"
values: "{{ item.update_values | default(item['values']) }}"
ttl: "{{ item.update_ttl | default(item.ttl) }}"
type: "{{ item.type }}"
register: result
- name: verify test update a dns record idempotence
assert:
that:
- result is not changed
- result.record['values'] == {{ item.update_values | default(item['values']) }}
- result.record.record == "{{ item.record }}"
- result.record.ttl == {{ item.update_ttl | default(item.ttl) }}
- result.record.type == "{{ item.type }}"