mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-10-23 20:44:00 -07:00
Some checks are pending
EOL CI / EOL Sanity (Ⓐ2.17) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.17+py3.10) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.17+py3.12) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.17+py3.7) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/3/) (push) Waiting to run
nox / Run extra sanity tests (push) Waiting to run
* use f-strings in module utils * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> * remove unused imports --------- Co-authored-by: Felix Fontein <felix@fontein.de>
239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
# Copyright (c) 2019 Gregory Thiemonge <gregory.thiemonge@gmail.com>
|
|
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
|
|
# SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from ansible.module_utils.common.text.converters import 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']
|
|
self.personal_access_token = module.params['personal_access_token']
|
|
|
|
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 += f'{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):
|
|
authorization_header = (
|
|
f'Bearer {self.personal_access_token}'
|
|
if self.personal_access_token
|
|
else f'Apikey {self.api_key}'
|
|
)
|
|
headers = {'Authorization': authorization_header,
|
|
'Content-Type': 'application/json'}
|
|
data = None
|
|
if payload:
|
|
try:
|
|
data = json.dumps(payload)
|
|
except Exception as e:
|
|
self.module.fail_json(msg=f"Failed to encode payload as JSON: {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 = f"API Error {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 += f"; Failed to parse API response with error {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 = f'/domains/{domain}/records'
|
|
if record:
|
|
url += f'/{record}'
|
|
if type:
|
|
url += f'/{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 = f'/domains/{domain}/records'
|
|
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 = f'/domains/{domain}/records/{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 = f'/domains/{domain}/records/{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
|