1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/memset_zone_record.py
Felix Fontein 9a16eaf9ba
Fix pylint and pep8 issues exposed by latest ansible-core's ansible-test sanity checks (#8720)
* Remove bad whitespace.

* 'Fixing' various used-before-assignment issues that pylint flagged.
2024-08-07 15:18:58 +02:00

394 lines
14 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2018, Simon Weald <ansible@simonweald.com>
# 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: memset_zone_record
author: "Simon Weald (@glitchcrab)"
short_description: Create and delete records in Memset DNS zones
notes:
- Zones can be thought of as a logical group of domains, all of which share the
same DNS records (i.e. they point to the same IP). An API key generated via the
Memset customer control panel is needed with the following minimum scope -
C(dns.zone_create), C(dns.zone_delete), C(dns.zone_list).
- Currently this module can only create one DNS record at a time. Multiple records
should be created using C(loop).
description:
- Manage DNS records in a Memset account.
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
state:
default: present
description:
- Indicates desired state of resource.
type: str
choices: [ absent, present ]
api_key:
required: true
description:
- The API key obtained from the Memset control panel.
type: str
address:
required: true
description:
- The address for this record (can be IP or text string depending on record type).
type: str
aliases: [ ip, data ]
priority:
description:
- C(SRV) and C(TXT) record priority, in the range 0 > 999 (inclusive).
type: int
default: 0
record:
required: false
description:
- The subdomain to create.
type: str
default: ''
type:
required: true
description:
- The type of DNS record to create.
choices: [ A, AAAA, CNAME, MX, NS, SRV, TXT ]
type: str
relative:
type: bool
default: false
description:
- If set then the current domain is added onto the address field for C(CNAME), C(MX), C(NS)
and C(SRV)record types.
ttl:
description:
- The record's TTL in seconds (will inherit zone's TTL if not explicitly set). This must be a
valid int from U(https://www.memset.com/apidocs/methods_dns.html#dns.zone_record_create).
default: 0
choices: [ 0, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400 ]
type: int
zone:
required: true
description:
- The name of the zone to which to add the record to.
type: str
'''
EXAMPLES = '''
# Create DNS record for www.domain.com
- name: Create DNS record
community.general.memset_zone_record:
api_key: dcf089a2896940da9ffefb307ef49ccd
state: present
zone: domain.com
type: A
record: www
address: 1.2.3.4
ttl: 300
relative: false
delegate_to: localhost
# create an SPF record for domain.com
- name: Create SPF record for domain.com
community.general.memset_zone_record:
api_key: dcf089a2896940da9ffefb307ef49ccd
state: present
zone: domain.com
type: TXT
address: "v=spf1 +a +mx +ip4:a1.2.3.4 ?all"
delegate_to: localhost
# create multiple DNS records
- name: Create multiple DNS records
community.general.memset_zone_record:
api_key: dcf089a2896940da9ffefb307ef49ccd
zone: "{{ item.zone }}"
type: "{{ item.type }}"
record: "{{ item.record }}"
address: "{{ item.address }}"
delegate_to: localhost
with_items:
- { 'zone': 'domain1.com', 'type': 'A', 'record': 'www', 'address': '1.2.3.4' }
- { 'zone': 'domain2.com', 'type': 'A', 'record': 'mail', 'address': '4.3.2.1' }
'''
RETURN = '''
memset_api:
description: Record info from the Memset API.
returned: when state == present
type: complex
contains:
address:
description: Record content (may be an IP, string or blank depending on record type).
returned: always
type: str
sample: 1.1.1.1
id:
description: Record ID.
returned: always
type: str
sample: "b0bb1ce851aeea6feeb2dc32fe83bf9c"
priority:
description: Priority for C(MX) and C(SRV) records.
returned: always
type: int
sample: 10
record:
description: Name of record.
returned: always
type: str
sample: "www"
relative:
description: Adds the current domain onto the address field for C(CNAME), C(MX), C(NS) and C(SRV) types.
returned: always
type: bool
sample: false
ttl:
description: Record TTL.
returned: always
type: int
sample: 10
type:
description: Record type.
returned: always
type: str
sample: AAAA
zone_id:
description: Zone ID.
returned: always
type: str
sample: "b0bb1ce851aeea6feeb2dc32fe83bf9c"
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.memset import get_zone_id
from ansible_collections.community.general.plugins.module_utils.memset import memset_api_call
def api_validation(args=None):
'''
Perform some validation which will be enforced by Memset's API (see:
https://www.memset.com/apidocs/methods_dns.html#dns.zone_record_create)
'''
failed_validation = False
error = None
# priority can only be integer 0 > 999
if not 0 <= args['priority'] <= 999:
failed_validation = True
error = 'Priority must be in the range 0 > 999 (inclusive).'
# data value must be max 250 chars
if len(args['address']) > 250:
failed_validation = True
error = "Address must be less than 250 characters in length."
# record value must be max 250 chars
if args['record']:
if len(args['record']) > 63:
failed_validation = True
error = "Record must be less than 63 characters in length."
# relative isn't used for all record types
if args['relative']:
if args['type'] not in ['CNAME', 'MX', 'NS', 'SRV']:
failed_validation = True
error = "Relative is only valid for CNAME, MX, NS and SRV record types."
# if any of the above failed then fail early
if failed_validation:
module.fail_json(failed=True, msg=error)
def create_zone_record(args=None, zone_id=None, records=None, payload=None):
'''
Sanity checking has already occurred prior to this function being
called, so we can go ahead and either create or update the record.
As defaults are defined for all values in the argument_spec, this
may cause some changes to occur as the defaults are enforced (if
the user has only configured required variables).
'''
has_changed, has_failed = False, False
msg, memset_api = None, None
# assemble the new record.
new_record = dict()
new_record['zone_id'] = zone_id
for arg in ['priority', 'address', 'relative', 'record', 'ttl', 'type']:
new_record[arg] = args[arg]
# if we have any matches, update them.
if records:
for zone_record in records:
# record exists, add ID to payload.
new_record['id'] = zone_record['id']
if zone_record == new_record:
# nothing to do; record is already correct so we populate
# the return var with the existing record's details.
memset_api = zone_record
return has_changed, has_failed, memset_api, msg
else:
# merge dicts ensuring we change any updated values
payload = zone_record.copy()
payload.update(new_record)
api_method = 'dns.zone_record_update'
if args['check_mode']:
has_changed = True
# return the new record to the user in the returned var.
memset_api = new_record
return has_changed, has_failed, memset_api, msg
has_failed, msg, response = memset_api_call(api_key=args['api_key'], api_method=api_method, payload=payload)
if not has_failed:
has_changed = True
memset_api = new_record
# empty msg as we don't want to return a boatload of json to the user.
msg = None
else:
# no record found, so we need to create it
api_method = 'dns.zone_record_create'
payload = new_record
if args['check_mode']:
has_changed = True
# populate the return var with the new record's details.
memset_api = new_record
return has_changed, has_failed, memset_api, msg
has_failed, msg, response = memset_api_call(api_key=args['api_key'], api_method=api_method, payload=payload)
if not has_failed:
has_changed = True
memset_api = new_record
# empty msg as we don't want to return a boatload of json to the user.
msg = None
return has_changed, has_failed, memset_api, msg
def delete_zone_record(args=None, records=None, payload=None):
'''
Matching records can be cleanly deleted without affecting other
resource types, so this is pretty simple to achieve.
'''
has_changed, has_failed = False, False
msg, memset_api = None, None
# if we have any matches, delete them.
if records:
for zone_record in records:
if args['check_mode']:
has_changed = True
return has_changed, has_failed, memset_api, msg
payload['id'] = zone_record['id']
api_method = 'dns.zone_record_delete'
has_failed, msg, response = memset_api_call(api_key=args['api_key'], api_method=api_method, payload=payload)
if not has_failed:
has_changed = True
memset_api = zone_record
# empty msg as we don't want to return a boatload of json to the user.
msg = None
return has_changed, has_failed, memset_api, msg
def create_or_delete(args=None):
'''
We need to perform some initial sanity checking and also look
up required info before handing it off to create or delete functions.
Check mode is integrated into the create or delete functions.
'''
has_failed, has_changed = False, False
msg, memset_api, stderr = None, None, None
retvals, payload = dict(), dict()
# get the zones and check if the relevant zone exists.
api_method = 'dns.zone_list'
_has_failed, msg, response = memset_api_call(api_key=args['api_key'], api_method=api_method)
if _has_failed:
# this is the first time the API is called; incorrect credentials will
# manifest themselves at this point so we need to ensure the user is
# informed of the reason.
retvals['failed'] = _has_failed
retvals['msg'] = msg
if response.status_code is not None:
retvals['stderr'] = "API returned an error: {0}" . format(response.status_code)
else:
retvals['stderr'] = response.stderr
return retvals
zone_exists, _msg, counter, zone_id = get_zone_id(zone_name=args['zone'], current_zones=response.json())
if not zone_exists:
has_failed = True
if counter == 0:
stderr = "DNS zone {0} does not exist." . format(args['zone'])
elif counter > 1:
stderr = "{0} matches multiple zones." . format(args['zone'])
retvals['failed'] = has_failed
retvals['msg'] = stderr
retvals['stderr'] = stderr
return retvals
# get a list of all records ( as we can't limit records by zone)
api_method = 'dns.zone_record_list'
_has_failed, _msg, response = memset_api_call(api_key=args['api_key'], api_method=api_method)
# find any matching records
records = [record for record in response.json() if record['zone_id'] == zone_id
and record['record'] == args['record'] and record['type'] == args['type']]
if args['state'] == 'present':
has_changed, has_failed, memset_api, msg = create_zone_record(args=args, zone_id=zone_id, records=records, payload=payload)
if args['state'] == 'absent':
has_changed, has_failed, memset_api, msg = delete_zone_record(args=args, records=records, payload=payload)
retvals['changed'] = has_changed
retvals['failed'] = has_failed
for val in ['msg', 'stderr', 'memset_api']:
if val is not None:
retvals[val] = eval(val)
return retvals
def main():
global module
module = AnsibleModule(
argument_spec=dict(
state=dict(required=False, default='present', choices=['present', 'absent'], type='str'),
api_key=dict(required=True, type='str', no_log=True),
zone=dict(required=True, type='str'),
type=dict(required=True, choices=['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'TXT'], type='str'),
address=dict(required=True, aliases=['ip', 'data'], type='str'),
record=dict(required=False, default='', type='str'),
ttl=dict(required=False, default=0, choices=[0, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400], type='int'),
priority=dict(required=False, default=0, type='int'),
relative=dict(required=False, default=False, type='bool')
),
supports_check_mode=True
)
# populate the dict with the user-provided vars.
args = dict()
for key, arg in module.params.items():
args[key] = arg
args['check_mode'] = module.check_mode
# perform some Memset API-specific validation
api_validation(args=args)
retvals = create_or_delete(args)
if retvals['failed']:
module.fail_json(**retvals)
else:
module.exit_json(**retvals)
if __name__ == '__main__':
main()