Initial commit

This commit is contained in:
Ansible Core Team
2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View File

@@ -0,0 +1,875 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: cloudflare_dns
author:
- Michael Gruener (@mgruener)
requirements:
- python >= 2.6
short_description: Manage Cloudflare DNS records
description:
- "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)"
options:
api_token:
description:
- API token.
- Required for api token authentication.
- "You can obtain your API token from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)"
type: str
required: false
account_api_key:
description:
- Account API key.
- Required for api keys authentication.
- "You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)"
type: str
required: false
aliases: [ account_api_token ]
account_email:
description:
- Account email. Required for api keys authentication.
type: str
required: false
algorithm:
description:
- Algorithm number.
- Required for C(type=DS) and C(type=SSHFP) when C(state=present).
type: int
cert_usage:
description:
- Certificate usage number.
- Required for C(type=TLSA) when C(state=present).
type: int
choices: [ 0, 1, 2, 3 ]
hash_type:
description:
- Hash type number.
- Required for C(type=DS), C(type=SSHFP) and C(type=TLSA) when C(state=present).
type: int
choices: [ 1, 2 ]
key_tag:
description:
- DNSSEC key tag.
- Needed for C(type=DS) when C(state=present).
type: int
port:
description:
- Service port.
- Required for C(type=SRV) and C(type=TLSA).
type: int
priority:
description:
- Record priority.
- Required for C(type=MX) and C(type=SRV)
default: 1
proto:
description:
- Service protocol. Required for C(type=SRV) and C(type=TLSA).
- Common values are TCP and UDP.
- Before Ansible 2.6 only TCP and UDP were available.
type: str
proxied:
description:
- Proxy through Cloudflare network or just use DNS.
type: bool
default: no
record:
description:
- Record to add.
- Required if C(state=present).
- Default is C(@) (e.g. the zone name).
type: str
default: '@'
aliases: [ name ]
selector:
description:
- Selector number.
- Required for C(type=TLSA) when C(state=present).
choices: [ 0, 1 ]
type: int
service:
description:
- Record service.
- Required for C(type=SRV)
solo:
description:
- Whether the record should be the only one for that record type and record name.
- Only use with C(state=present).
- This will delete all other records with the same record name and type.
type: bool
state:
description:
- Whether the record(s) should exist or not.
type: str
choices: [ absent, present ]
default: present
timeout:
description:
- Timeout for Cloudflare API calls.
type: int
default: 30
ttl:
description:
- The TTL to give the new record.
- Must be between 120 and 2,147,483,647 seconds, or 1 for automatic.
type: int
default: 1
type:
description:
- The type of DNS record to create. Required if C(state=present).
- C(type=DS), C(type=SSHFP) and C(type=TLSA) added in Ansible 2.7.
type: str
choices: [ A, AAAA, CNAME, DS, MX, NS, SPF, SRV, SSHFP, TLSA, TXT ]
value:
description:
- The record value.
- Required for C(state=present).
type: str
aliases: [ content ]
weight:
description:
- Service weight.
- Required for C(type=SRV).
type: int
default: 1
zone:
description:
- The name of the Zone to work with (e.g. "example.com").
- The Zone must already exist.
type: str
required: true
aliases: [ domain ]
'''
EXAMPLES = r'''
- name: Create a test.example.net A record to point to 127.0.0.1
cloudflare_dns:
zone: example.net
record: test
type: A
value: 127.0.0.1
account_email: test@example.com
account_api_key: dummyapitoken
register: record
- name: Create a record using api token
cloudflare_dns:
zone: example.net
record: test
type: A
value: 127.0.0.1
api_token: dummyapitoken
- name: Create a example.net CNAME record to example.com
cloudflare_dns:
zone: example.net
type: CNAME
value: example.com
account_email: test@example.com
account_api_key: dummyapitoken
state: present
- name: Change its TTL
cloudflare_dns:
zone: example.net
type: CNAME
value: example.com
ttl: 600
account_email: test@example.com
account_api_key: dummyapitoken
state: present
- name: Delete the record
cloudflare_dns:
zone: example.net
type: CNAME
value: example.com
account_email: test@example.com
account_api_key: dummyapitoken
state: absent
- name: create a example.net CNAME record to example.com and proxy through Cloudflare's network
cloudflare_dns:
zone: example.net
type: CNAME
value: example.com
proxied: yes
account_email: test@example.com
account_api_key: dummyapitoken
state: present
# This deletes all other TXT records named "test.example.net"
- name: Create TXT record "test.example.net" with value "unique value"
cloudflare_dns:
domain: example.net
record: test
type: TXT
value: unique value
solo: true
account_email: test@example.com
account_api_key: dummyapitoken
state: present
- name: Create an SRV record _foo._tcp.example.net
cloudflare_dns:
domain: example.net
service: foo
proto: tcp
port: 3500
priority: 10
weight: 20
type: SRV
value: fooserver.example.net
- name: Create a SSHFP record login.example.com
cloudflare_dns:
zone: example.com
record: login
type: SSHFP
algorithm: 4
hash_type: 2
value: 9dc1d6742696d2f51ca1f1a78b3d16a840f7d111eb9454239e70db31363f33e1
- name: Create a TLSA record _25._tcp.mail.example.com
cloudflare_dns:
zone: example.com
record: mail
port: 25
proto: tcp
type: TLSA
cert_usage: 3
selector: 1
hash_type: 1
value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3
- name: Create a DS record for subdomain.example.com
cloudflare_dns:
zone: example.com
record: subdomain
type: DS
key_tag: 5464
algorithm: 8
hash_type: 2
value: B4EB5AC4467D2DFB3BAF9FB9961DC1B6FED54A58CDFAA3E465081EC86F89BFAB
'''
RETURN = r'''
record:
description: A dictionary containing the record data.
returned: success, except on record deletion
type: complex
contains:
content:
description: The record content (details depend on record type).
returned: success
type: str
sample: 192.0.2.91
created_on:
description: The record creation date.
returned: success
type: str
sample: "2016-03-25T19:09:42.516553Z"
data:
description: Additional record data.
returned: success, if type is SRV, DS, SSHFP or TLSA
type: dict
sample: {
name: "jabber",
port: 8080,
priority: 10,
proto: "_tcp",
service: "_xmpp",
target: "jabberhost.sample.com",
weight: 5,
}
id:
description: The record ID.
returned: success
type: str
sample: f9efb0549e96abcb750de63b38c9576e
locked:
description: No documentation available.
returned: success
type: bool
sample: False
meta:
description: No documentation available.
returned: success
type: dict
sample: { auto_added: false }
modified_on:
description: Record modification date.
returned: success
type: str
sample: "2016-03-25T19:09:42.516553Z"
name:
description: The record name as FQDN (including _service and _proto for SRV).
returned: success
type: str
sample: www.sample.com
priority:
description: Priority of the MX record.
returned: success, if type is MX
type: int
sample: 10
proxiable:
description: Whether this record can be proxied through Cloudflare.
returned: success
type: bool
sample: False
proxied:
description: Whether the record is proxied through Cloudflare.
returned: success
type: bool
sample: False
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
zone_id:
description: The ID of the zone containing the record.
returned: success
type: str
sample: abcede0bf9f0066f94029d2e6b73856a
zone_name:
description: The name of the zone containing the record.
returned: success
type: str
sample: sample.com
'''
import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.urls import fetch_url
def lowercase_string(param):
if not isinstance(param, str):
return param
return param.lower()
class CloudflareAPI(object):
cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
changed = False
def __init__(self, module):
self.module = module
self.api_token = module.params['api_token']
self.account_api_key = module.params['account_api_key']
self.account_email = module.params['account_email']
self.algorithm = module.params['algorithm']
self.cert_usage = module.params['cert_usage']
self.hash_type = module.params['hash_type']
self.key_tag = module.params['key_tag']
self.port = module.params['port']
self.priority = module.params['priority']
self.proto = lowercase_string(module.params['proto'])
self.proxied = module.params['proxied']
self.selector = module.params['selector']
self.record = lowercase_string(module.params['record'])
self.service = lowercase_string(module.params['service'])
self.is_solo = module.params['solo']
self.state = module.params['state']
self.timeout = module.params['timeout']
self.ttl = module.params['ttl']
self.type = module.params['type']
self.value = module.params['value']
self.weight = module.params['weight']
self.zone = lowercase_string(module.params['zone'])
if self.record == '@':
self.record = self.zone
if (self.type in ['CNAME', 'NS', 'MX', 'SRV']) and (self.value is not None):
self.value = self.value.rstrip('.').lower()
if (self.type == 'AAAA') and (self.value is not None):
self.value = self.value.lower()
if (self.type == 'SRV'):
if (self.proto is not None) and (not self.proto.startswith('_')):
self.proto = '_' + self.proto
if (self.service is not None) and (not self.service.startswith('_')):
self.service = '_' + self.service
if (self.type == 'TLSA'):
if (self.proto is not None) and (not self.proto.startswith('_')):
self.proto = '_' + self.proto
if (self.port is not None):
self.port = '_' + str(self.port)
if not self.record.endswith(self.zone):
self.record = self.record + '.' + self.zone
if (self.type == 'DS'):
if self.record == self.zone:
self.module.fail_json(msg="DS records only apply to subdomains.")
def _cf_simple_api_call(self, api_call, method='GET', payload=None):
if self.api_token:
headers = {
'Authorization': 'Bearer ' + self.api_token,
'Content-Type': 'application/json',
}
else:
headers = {
'X-Auth-Email': self.account_email,
'X-Auth-Key': self.account_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.cf_api_endpoint + api_call,
headers=headers,
data=data,
method=method,
timeout=self.timeout)
if info['status'] not in [200, 304, 400, 401, 403, 429, 405, 415]:
self.module.fail_json(msg="Failed API call {0}; got unexpected HTTP code {1}".format(api_call, info['status']))
error_msg = ''
if info['status'] == 401:
# Unauthorized
error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
elif info['status'] == 403:
# Forbidden
error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
elif info['status'] == 429:
# Too many requests
error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
elif info['status'] == 405:
# Method not allowed
error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
elif info['status'] == 415:
# Unsupported Media Type
error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
elif info['status'] == 400:
# Bad Request
error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
result = None
try:
content = resp.read()
except AttributeError:
if info['body']:
content = info['body']
else:
error_msg += "; The API response was empty"
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)
# Without a valid/parsed JSON response no more error processing can be done
if result is None:
self.module.fail_json(msg=error_msg)
if not result['success']:
error_msg += "; Error details: "
for error in result['errors']:
error_msg += "code: {0}, error: {1}; ".format(error['code'], error['message'])
if 'error_chain' in error:
for chain_error in error['error_chain']:
error_msg += "code: {0}, error: {1}; ".format(chain_error['code'], chain_error['message'])
self.module.fail_json(msg=error_msg)
return result, info['status']
def _cf_api_call(self, api_call, method='GET', payload=None):
result, status = self._cf_simple_api_call(api_call, method, payload)
data = result['result']
if 'result_info' in result:
pagination = result['result_info']
if pagination['total_pages'] > 1:
next_page = int(pagination['page']) + 1
parameters = ['page={0}'.format(next_page)]
# strip "page" parameter from call parameters (if there are any)
if '?' in api_call:
raw_api_call, query = api_call.split('?', 1)
parameters += [param for param in query.split('&') if not param.startswith('page')]
else:
raw_api_call = api_call
while next_page <= pagination['total_pages']:
raw_api_call += '?' + '&'.join(parameters)
result, status = self._cf_simple_api_call(raw_api_call, method, payload)
data += result['result']
next_page += 1
return data, status
def _get_zone_id(self, zone=None):
if not zone:
zone = self.zone
zones = self.get_zones(zone)
if len(zones) > 1:
self.module.fail_json(msg="More than one zone matches {0}".format(zone))
if len(zones) < 1:
self.module.fail_json(msg="No zone found with name {0}".format(zone))
return zones[0]['id']
def get_zones(self, name=None):
if not name:
name = self.zone
param = ''
if name:
param = '?' + urlencode({'name': name})
zones, status = self._cf_api_call('/zones' + param)
return zones
def get_dns_records(self, zone_name=None, type=None, record=None, value=''):
if not zone_name:
zone_name = self.zone
if not type:
type = self.type
if not record:
record = self.record
# necessary because None as value means to override user
# set module value
if (not value) and (value is not None):
value = self.value
zone_id = self._get_zone_id()
api_call = '/zones/{0}/dns_records'.format(zone_id)
query = {}
if type:
query['type'] = type
if record:
query['name'] = record
if value:
query['content'] = value
if query:
api_call += '?' + urlencode(query)
records, status = self._cf_api_call(api_call)
return records
def delete_dns_records(self, **kwargs):
params = {}
for param in ['port', 'proto', 'service', 'solo', 'type', 'record', 'value', 'weight', 'zone',
'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
if param in kwargs:
params[param] = kwargs[param]
else:
params[param] = getattr(self, param)
records = []
content = params['value']
search_record = params['record']
if params['type'] == 'SRV':
if not (params['value'] is None or params['value'] == ''):
content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
elif params['type'] == 'DS':
if not (params['value'] is None or params['value'] == ''):
content = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
elif params['type'] == 'SSHFP':
if not (params['value'] is None or params['value'] == ''):
content = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
elif params['type'] == 'TLSA':
if not (params['value'] is None or params['value'] == ''):
content = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
if params['solo']:
search_value = None
else:
search_value = content
records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
for rr in records:
if params['solo']:
if not ((rr['type'] == params['type']) and (rr['name'] == search_record) and (rr['content'] == content)):
self.changed = True
if not self.module.check_mode:
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
else:
self.changed = True
if not self.module.check_mode:
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
return self.changed
def ensure_dns_record(self, **kwargs):
params = {}
for param in ['port', 'priority', 'proto', 'proxied', 'service', 'ttl', 'type', 'record', 'value', 'weight', 'zone',
'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
if param in kwargs:
params[param] = kwargs[param]
else:
params[param] = getattr(self, param)
search_value = params['value']
search_record = params['record']
new_record = None
if (params['type'] is None) or (params['record'] is None):
self.module.fail_json(msg="You must provide a type and a record to create a new record")
if (params['type'] in ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SPF']):
if not params['value']:
self.module.fail_json(msg="You must provide a non-empty value to create this record type")
# there can only be one CNAME per record
# ignoring the value when searching for existing
# CNAME records allows us to update the value if it
# changes
if params['type'] == 'CNAME':
search_value = None
new_record = {
"type": params['type'],
"name": params['record'],
"content": params['value'],
"ttl": params['ttl']
}
if (params['type'] in ['A', 'AAAA', 'CNAME']):
new_record["proxied"] = params["proxied"]
if params['type'] == 'MX':
for attr in [params['priority'], params['value']]:
if (attr is None) or (attr == ''):
self.module.fail_json(msg="You must provide priority and a value to create this record type")
new_record = {
"type": params['type'],
"name": params['record'],
"content": params['value'],
"priority": params['priority'],
"ttl": params['ttl']
}
if params['type'] == 'SRV':
for attr in [params['port'], params['priority'], params['proto'], params['service'], params['weight'], params['value']]:
if (attr is None) or (attr == ''):
self.module.fail_json(msg="You must provide port, priority, proto, service, weight and a value to create this record type")
srv_data = {
"target": params['value'],
"port": params['port'],
"weight": params['weight'],
"priority": params['priority'],
"name": params['record'][:-len('.' + params['zone'])],
"proto": params['proto'],
"service": params['service']
}
new_record = {"type": params['type'], "ttl": params['ttl'], 'data': srv_data}
search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
if params['type'] == 'DS':
for attr in [params['key_tag'], params['algorithm'], params['hash_type'], params['value']]:
if (attr is None) or (attr == ''):
self.module.fail_json(msg="You must provide key_tag, algorithm, hash_type and a value to create this record type")
ds_data = {
"key_tag": params['key_tag'],
"algorithm": params['algorithm'],
"digest_type": params['hash_type'],
"digest": params['value'],
}
new_record = {
"type": params['type'],
"name": params['record'],
'data': ds_data,
"ttl": params['ttl'],
}
search_value = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
if params['type'] == 'SSHFP':
for attr in [params['algorithm'], params['hash_type'], params['value']]:
if (attr is None) or (attr == ''):
self.module.fail_json(msg="You must provide algorithm, hash_type and a value to create this record type")
sshfp_data = {
"fingerprint": params['value'],
"type": params['hash_type'],
"algorithm": params['algorithm'],
}
new_record = {
"type": params['type'],
"name": params['record'],
'data': sshfp_data,
"ttl": params['ttl'],
}
search_value = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
if params['type'] == 'TLSA':
for attr in [params['port'], params['proto'], params['cert_usage'], params['selector'], params['hash_type'], params['value']]:
if (attr is None) or (attr == ''):
self.module.fail_json(msg="You must provide port, proto, cert_usage, selector, hash_type and a value to create this record type")
search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
tlsa_data = {
"usage": params['cert_usage'],
"selector": params['selector'],
"matching_type": params['hash_type'],
"certificate": params['value'],
}
new_record = {
"type": params['type'],
"name": search_record,
'data': tlsa_data,
"ttl": params['ttl'],
}
search_value = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
zone_id = self._get_zone_id(params['zone'])
records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
# in theory this should be impossible as cloudflare does not allow
# the creation of duplicate records but lets cover it anyways
if len(records) > 1:
self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!")
# record already exists, check if it must be updated
if len(records) == 1:
cur_record = records[0]
do_update = False
if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl']):
do_update = True
if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']):
do_update = True
if ('proxied' in new_record) and ('proxied' in cur_record) and (cur_record['proxied'] != params['proxied']):
do_update = True
if ('data' in new_record) and ('data' in cur_record):
if (cur_record['data'] != new_record['data']):
do_update = True
if (params['type'] == 'CNAME') and (cur_record['content'] != new_record['content']):
do_update = True
if do_update:
if self.module.check_mode:
result = new_record
else:
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id, records[0]['id']), 'PUT', new_record)
self.changed = True
return result, self.changed
else:
return records, self.changed
if self.module.check_mode:
result = new_record
else:
result, info = self._cf_api_call('/zones/{0}/dns_records'.format(zone_id), 'POST', new_record)
self.changed = True
return result, self.changed
def main():
module = AnsibleModule(
argument_spec=dict(
api_token=dict(type='str', required=False, no_log=True),
account_api_key=dict(type='str', required=False, no_log=True, aliases=['account_api_token']),
account_email=dict(type='str', required=False),
algorithm=dict(type='int'),
cert_usage=dict(type='int', choices=[0, 1, 2, 3]),
hash_type=dict(type='int', choices=[1, 2]),
key_tag=dict(type='int'),
port=dict(type='int'),
priority=dict(type='int', default=1),
proto=dict(type='str'),
proxied=dict(type='bool', default=False),
record=dict(type='str', default='@', aliases=['name']),
selector=dict(type='int', choices=[0, 1]),
service=dict(type='str'),
solo=dict(type='bool'),
state=dict(type='str', default='present', choices=['absent', 'present']),
timeout=dict(type='int', default=30),
ttl=dict(type='int', default=1),
type=dict(type='str', choices=['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT']),
value=dict(type='str', aliases=['content']),
weight=dict(type='int', default=1),
zone=dict(type='str', required=True, aliases=['domain']),
),
supports_check_mode=True,
required_if=[
('state', 'present', ['record', 'type', 'value']),
('state', 'absent', ['record']),
('type', 'SRV', ['proto', 'service']),
('type', 'TLSA', ['proto', 'port']),
],
)
if not module.params['api_token'] and not (module.params['account_api_key'] and module.params['account_email']):
module.fail_json(msg="Either api_token or account_api_key and account_email params are required.")
if module.params['type'] == 'SRV':
if not ((module.params['weight'] is not None and module.params['port'] is not None
and not (module.params['value'] is None or module.params['value'] == ''))
or (module.params['weight'] is None and module.params['port'] is None
and (module.params['value'] is None or module.params['value'] == ''))):
module.fail_json(msg="For SRV records the params weight, port and value all need to be defined, or not at all.")
if module.params['type'] == 'SSHFP':
if not ((module.params['algorithm'] is not None and module.params['hash_type'] is not None
and not (module.params['value'] is None or module.params['value'] == ''))
or (module.params['algorithm'] is None and module.params['hash_type'] is None
and (module.params['value'] is None or module.params['value'] == ''))):
module.fail_json(msg="For SSHFP records the params algorithm, hash_type and value all need to be defined, or not at all.")
if module.params['type'] == 'TLSA':
if not ((module.params['cert_usage'] is not None and module.params['selector'] is not None and module.params['hash_type'] is not None
and not (module.params['value'] is None or module.params['value'] == ''))
or (module.params['cert_usage'] is None and module.params['selector'] is None and module.params['hash_type'] is None
and (module.params['value'] is None or module.params['value'] == ''))):
module.fail_json(msg="For TLSA records the params cert_usage, selector, hash_type and value all need to be defined, or not at all.")
if module.params['type'] == 'DS':
if not ((module.params['key_tag'] is not None and module.params['algorithm'] is not None and module.params['hash_type'] is not None
and not (module.params['value'] is None or module.params['value'] == ''))
or (module.params['key_tag'] is None and module.params['algorithm'] is None and module.params['hash_type'] is None
and (module.params['value'] is None or module.params['value'] == ''))):
module.fail_json(msg="For DS records the params key_tag, algorithm, hash_type and value all need to be defined, or not at all.")
changed = False
cf_api = CloudflareAPI(module)
# sanity checks
if cf_api.is_solo and cf_api.state == 'absent':
module.fail_json(msg="solo=true can only be used with state=present")
# perform add, delete or update (only the TTL can be updated) of one or
# more records
if cf_api.state == 'present':
# delete all records matching record name + type
if cf_api.is_solo:
changed = cf_api.delete_dns_records(solo=cf_api.is_solo)
result, changed = cf_api.ensure_dns_record()
if isinstance(result, list):
module.exit_json(changed=changed, result={'record': result[0]})
module.exit_json(changed=changed, result={'record': result})
else:
# force solo to False, just to be sure
changed = cf_api.delete_dns_records(solo=False)
module.exit_json(changed=changed)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,343 @@
#!/usr/bin/python
#
# Copyright: Ansible Project
#
# 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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
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/)."
notes:
- DNSimple API v1 is deprecated. Please install dnsimple-python>=1.0.0 which uses v2 API.
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)."
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
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' ]
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: no
requirements:
- "dnsimple >= 1.0.0"
author: "Alex Coomans (@drcapulet)"
'''
EXAMPLES = '''
- name: Authenticate using email and API token and fetch all domains
dnsimple:
account_email: test@example.com
account_api_token: dummyapitoken
delegate_to: localhost
- name: Fetch my.com domain records
dnsimple:
domain: my.com
state: present
delegate_to: localhost
register: records
- name: Delete a domain
dnsimple:
domain: my.com
state: absent
delegate_to: localhost
- name: Create a test.my.com A record to point to 127.0.0.1
dnsimple:
domain: my.com
record: test
type: A
value: 127.0.0.1
delegate_to: localhost
register: record
- name: Delete record using record_ids
dnsimple:
domain: my.com
record_ids: '{{ record["id"] }}'
state: absent
delegate_to: localhost
- name: Create a my.com CNAME record to example.com
dnsimple:
domain: my.com
record: ''
type: CNAME
value: example.com
state: present
delegate_to: localhost
- name: change TTL value for a record
dnsimple:
domain: my.com
record: ''
type: CNAME
value: example.com
ttl: 600
state: present
delegate_to: localhost
- name: Delete the record
dnsimple:
domain: my.com
record: ''
type: CNAME
value: example.com
state: absent
delegate_to: localhost
'''
RETURN = r"""# """
import os
import traceback
from distutils.version import LooseVersion
DNSIMPLE_IMP_ERR = None
try:
from dnsimple import DNSimple
from dnsimple.dnsimple import __version__ as dnsimple_version
from dnsimple.dnsimple import DNSimpleException
HAS_DNSIMPLE = True
except ImportError:
DNSIMPLE_IMP_ERR = traceback.format_exc()
HAS_DNSIMPLE = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
def main():
module = AnsibleModule(
argument_spec=dict(
account_email=dict(type='str'),
account_api_token=dict(type='str', no_log=True),
domain=dict(type='str'),
record=dict(type='str'),
record_ids=dict(type='list'),
type=dict(type='str', choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO',
'POOL']),
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),
),
required_together=[
['record', 'value']
],
supports_check_mode=True,
)
if not HAS_DNSIMPLE:
module.fail_json(msg=missing_required_lib('dnsimple'), exception=DNSIMPLE_IMP_ERR)
if LooseVersion(dnsimple_version) < LooseVersion('1.0.0'):
module.fail_json(msg="Current version of dnsimple Python module [%s] uses 'v1' API which is deprecated."
" Please upgrade to version 1.0.0 and above to use dnsimple 'v2' API." % dnsimple_version)
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')
if account_email and account_api_token:
client = DNSimple(email=account_email, api_token=account_api_token)
elif os.environ.get('DNSIMPLE_EMAIL') and os.environ.get('DNSIMPLE_API_TOKEN'):
client = DNSimple(email=os.environ.get('DNSIMPLE_EMAIL'), api_token=os.environ.get('DNSIMPLE_API_TOKEN'))
else:
client = DNSimple()
try:
# Let's figure out what operation we want to do
# No domain, return a list
if not domain:
domains = client.domains()
module.exit_json(changed=False, result=[d['domain'] for d in domains])
# Domain & No record
if domain and record is None and not record_ids:
domains = [d['domain'] for d in client.domains()]
if domain.isdigit():
dr = next((d for d in domains if d['id'] == int(domain)), None)
else:
dr = next((d for d in domains if d['name'] == domain), None)
if state == 'present':
if dr:
module.exit_json(changed=False, result=dr)
else:
if module.check_mode:
module.exit_json(changed=True)
else:
module.exit_json(changed=True, result=client.add_domain(domain)['domain'])
# state is absent
else:
if dr:
if not module.check_mode:
client.delete(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 domain and record is not None:
records = [r['record'] for r in client.records(str(domain), params={'name': record})]
if not record_type:
module.fail_json(msg="Missing the record type")
if not value:
module.fail_json(msg="Missing the record value")
rr = next((r for r in records 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 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:
client.delete_record(str(domain), rid)
changed = True
if rr:
# check if we need to update
if rr['ttl'] != ttl or rr['priority'] != priority:
data = {}
if ttl:
data['ttl'] = ttl
if priority:
data['priority'] = priority
if module.check_mode:
module.exit_json(changed=True)
else:
module.exit_json(changed=True, result=client.update_record(str(domain), str(rr['id']), data)['record'])
else:
module.exit_json(changed=changed, result=rr)
else:
# create it
data = {
'name': record,
'type': record_type,
'content': value,
}
if ttl:
data['ttl'] = ttl
if priority:
data['priority'] = priority
if module.check_mode:
module.exit_json(changed=True)
else:
module.exit_json(changed=True, result=client.add_record(str(domain), data)['record'])
# state is absent
else:
if rr:
if not module.check_mode:
client.delete_record(str(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 domain and record_ids:
current_records = [str(r['record']['id']) for r in client.records(str(domain))]
wanted_records = [str(r) for r in record_ids]
if state == 'present':
difference = list(set(wanted_records) - set(current_records))
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_records) & set(current_records))
if difference:
if not module.check_mode:
for rid in difference:
client.delete_record(str(domain), rid)
module.exit_json(changed=True)
else:
module.exit_json(changed=False)
except DNSimpleException as e:
module.fail_json(msg="Unable to contact DNSimple: %s" % e.message)
module.fail_json(msg="Unknown what you wanted me to do")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,697 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: Ansible Project
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: dnsmadeeasy
short_description: Interface with dnsmadeeasy.com (a DNS hosting service).
description:
- >
Manages DNS records via the v2 REST API of the DNS Made Easy service. It handles records only; there is no manipulation of domains or
monitor/account support yet. See: U(https://www.dnsmadeeasy.com/integration/restapi/)
options:
account_key:
description:
- Account API Key.
required: true
account_secret:
description:
- Account Secret Key.
required: true
domain:
description:
- Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNS Made Easy (e.g. "839989") for faster
resolution
required: true
sandbox:
description:
- Decides if the sandbox API should be used. Otherwise (default) the production API of DNS Made Easy is used.
type: bool
default: 'no'
record_name:
description:
- Record name to get/create/delete/update. If record_name is not specified; all records for the domain will be returned in "result" regardless
of the state argument.
record_type:
description:
- Record type.
choices: [ 'A', 'AAAA', 'CNAME', 'ANAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT' ]
record_value:
description:
- >
Record value. HTTPRED: <redirection URL>, MX: <priority> <target name>, NS: <name server>, PTR: <target name>,
SRV: <priority> <weight> <port> <target name>, TXT: <text value>"
- >
If record_value is not specified; no changes will be made and the record will be returned in 'result'
(in other words, this module can be used to fetch a record's current id, type, and ttl)
record_ttl:
description:
- record's "Time to live". Number of seconds the record remains cached in DNS servers.
default: 1800
state:
description:
- whether the record should exist or not
required: true
choices: [ 'present', 'absent' ]
validate_certs:
description:
- If C(no), SSL certificates will not be validated. This should only be used
on personally controlled sites using self-signed certificates.
type: bool
default: 'yes'
monitor:
description:
- If C(yes), add or change the monitor. This is applicable only for A records.
type: bool
default: 'no'
systemDescription:
description:
- Description used by the monitor.
required: true
default: ''
maxEmails:
description:
- Number of emails sent to the contact list by the monitor.
required: true
default: 1
protocol:
description:
- Protocol used by the monitor.
required: true
default: 'HTTP'
choices: ['TCP', 'UDP', 'HTTP', 'DNS', 'SMTP', 'HTTPS']
port:
description:
- Port used by the monitor.
required: true
default: 80
sensitivity:
description:
- Number of checks the monitor performs before a failover occurs where Low = 8, Medium = 5,and High = 3.
required: true
default: 'Medium'
choices: ['Low', 'Medium', 'High']
contactList:
description:
- Name or id of the contact list that the monitor will notify.
- The default C('') means the Account Owner.
required: true
default: ''
httpFqdn:
description:
- The fully qualified domain name used by the monitor.
httpFile:
description:
- The file at the Fqdn that the monitor queries for HTTP or HTTPS.
httpQueryString:
description:
- The string in the httpFile that the monitor queries for HTTP or HTTPS.
failover:
description:
- If C(yes), add or change the failover. This is applicable only for A records.
type: bool
default: 'no'
autoFailover:
description:
- If true, fallback to the primary IP address is manual after a failover.
- If false, fallback to the primary IP address is automatic after a failover.
type: bool
default: 'no'
ip1:
description:
- Primary IP address for the failover.
- Required if adding or changing the monitor or failover.
ip2:
description:
- Secondary IP address for the failover.
- Required if adding or changing the failover.
ip3:
description:
- Tertiary IP address for the failover.
ip4:
description:
- Quaternary IP address for the failover.
ip5:
description:
- Quinary IP address for the failover.
notes:
- The DNS Made Easy service requires that machines interacting with the API have the proper time and timezone set. Be sure you are within a few
seconds of actual time by using NTP.
- This module returns record(s) and monitor(s) in the "result" element when 'state' is set to 'present'.
These values can be be registered and used in your playbooks.
- Only A records can have a monitor or failover.
- To add failover, the 'failover', 'autoFailover', 'port', 'protocol', 'ip1', and 'ip2' options are required.
- To add monitor, the 'monitor', 'port', 'protocol', 'maxEmails', 'systemDescription', and 'ip1' options are required.
- The monitor and the failover will share 'port', 'protocol', and 'ip1' options.
requirements: [ hashlib, hmac ]
author: "Brice Burgess (@briceburg)"
'''
EXAMPLES = '''
# fetch my.com domain records
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
register: response
# create / ensure the presence of a record
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
# update the previously created record
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_value: 192.0.2.23
# fetch a specific record
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
register: response
# delete a record / ensure it is absent
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
record_type: A
state: absent
record_name: test
# Add a failover
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
failover: True
ip1: 127.0.0.2
ip2: 127.0.0.3
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
failover: True
ip1: 127.0.0.2
ip2: 127.0.0.3
ip3: 127.0.0.4
ip4: 127.0.0.5
ip5: 127.0.0.6
# Add a monitor
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
monitor: yes
ip1: 127.0.0.2
protocol: HTTP # default
port: 80 # default
maxEmails: 1
systemDescription: Monitor Test A record
contactList: my contact list
# Add a monitor with http options
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
monitor: yes
ip1: 127.0.0.2
protocol: HTTP # default
port: 80 # default
maxEmails: 1
systemDescription: Monitor Test A record
contactList: 1174 # contact list id
httpFqdn: http://my.com
httpFile: example
httpQueryString: some string
# Add a monitor and a failover
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
failover: True
ip1: 127.0.0.2
ip2: 127.0.0.3
monitor: yes
protocol: HTTPS
port: 443
maxEmails: 1
systemDescription: monitoring my.com status
contactList: emergencycontacts
# Remove a failover
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
failover: no
# Remove a monitor
- dnsmadeeasy:
account_key: key
account_secret: secret
domain: my.com
state: present
record_name: test
record_type: A
record_value: 127.0.0.1
monitor: no
'''
# ============================================
# DNSMadeEasy module specific support methods.
#
import json
import hashlib
import hmac
import locale
from time import strftime, gmtime
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.six import string_types
class DME2(object):
def __init__(self, apikey, secret, domain, sandbox, module):
self.module = module
self.api = apikey
self.secret = secret
if sandbox:
self.baseurl = 'https://api.sandbox.dnsmadeeasy.com/V2.0/'
self.module.warn(warning="Sandbox is enabled. All actions are made against the URL %s" % self.baseurl)
else:
self.baseurl = 'https://api.dnsmadeeasy.com/V2.0/'
self.domain = str(domain)
self.domain_map = None # ["domain_name"] => ID
self.record_map = None # ["record_name"] => ID
self.records = None # ["record_ID"] => <record>
self.all_records = None
self.contactList_map = None # ["contactList_name"] => ID
# Lookup the domain ID if passed as a domain name vs. ID
if not self.domain.isdigit():
self.domain = self.getDomainByName(self.domain)['id']
self.record_url = 'dns/managed/' + str(self.domain) + '/records'
self.monitor_url = 'monitor'
self.contactList_url = 'contactList'
def _headers(self):
currTime = self._get_date()
hashstring = self._create_hash(currTime)
headers = {'x-dnsme-apiKey': self.api,
'x-dnsme-hmac': hashstring,
'x-dnsme-requestDate': currTime,
'content-type': 'application/json'}
return headers
def _get_date(self):
locale.setlocale(locale.LC_TIME, 'C')
return strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())
def _create_hash(self, rightnow):
return hmac.new(self.secret.encode(), rightnow.encode(), hashlib.sha1).hexdigest()
def query(self, resource, method, data=None):
url = self.baseurl + resource
if data and not isinstance(data, string_types):
data = urlencode(data)
response, info = fetch_url(self.module, url, data=data, method=method, headers=self._headers())
if info['status'] not in (200, 201, 204):
self.module.fail_json(msg="%s returned %s, with body: %s" % (url, info['status'], info['msg']))
try:
return json.load(response)
except Exception:
return {}
def getDomain(self, domain_id):
if not self.domain_map:
self._instMap('domain')
return self.domains.get(domain_id, False)
def getDomainByName(self, domain_name):
if not self.domain_map:
self._instMap('domain')
return self.getDomain(self.domain_map.get(domain_name, 0))
def getDomains(self):
return self.query('dns/managed', 'GET')['data']
def getRecord(self, record_id):
if not self.record_map:
self._instMap('record')
return self.records.get(record_id, False)
# Try to find a single record matching this one.
# How we do this depends on the type of record. For instance, there
# can be several MX records for a single record_name while there can
# only be a single CNAME for a particular record_name. Note also that
# there can be several records with different types for a single name.
def getMatchingRecord(self, record_name, record_type, record_value):
# Get all the records if not already cached
if not self.all_records:
self.all_records = self.getRecords()
if record_type in ["CNAME", "ANAME", "HTTPRED", "PTR"]:
for result in self.all_records:
if result['name'] == record_name and result['type'] == record_type:
return result
return False
elif record_type in ["A", "AAAA", "MX", "NS", "TXT", "SRV"]:
for result in self.all_records:
if record_type == "MX":
value = record_value.split(" ")[1]
elif record_type == "SRV":
value = record_value.split(" ")[3]
else:
value = record_value
if result['name'] == record_name and result['type'] == record_type and result['value'] == value:
return result
return False
else:
raise Exception('record_type not yet supported')
def getRecords(self):
return self.query(self.record_url, 'GET')['data']
def _instMap(self, type):
# @TODO cache this call so it's executed only once per ansible execution
map = {}
results = {}
# iterate over e.g. self.getDomains() || self.getRecords()
for result in getattr(self, 'get' + type.title() + 's')():
map[result['name']] = result['id']
results[result['id']] = result
# e.g. self.domain_map || self.record_map
setattr(self, type + '_map', map)
setattr(self, type + 's', results) # e.g. self.domains || self.records
def prepareRecord(self, data):
return json.dumps(data, separators=(',', ':'))
def createRecord(self, data):
# @TODO update the cache w/ resultant record + id when impleneted
return self.query(self.record_url, 'POST', data)
def updateRecord(self, record_id, data):
# @TODO update the cache w/ resultant record + id when impleneted
return self.query(self.record_url + '/' + str(record_id), 'PUT', data)
def deleteRecord(self, record_id):
# @TODO remove record from the cache when impleneted
return self.query(self.record_url + '/' + str(record_id), 'DELETE')
def getMonitor(self, record_id):
return self.query(self.monitor_url + '/' + str(record_id), 'GET')
def updateMonitor(self, record_id, data):
return self.query(self.monitor_url + '/' + str(record_id), 'PUT', data)
def prepareMonitor(self, data):
return json.dumps(data, separators=(',', ':'))
def getContactList(self, contact_list_id):
if not self.contactList_map:
self._instMap('contactList')
return self.contactLists.get(contact_list_id, False)
def getContactlists(self):
return self.query(self.contactList_url, 'GET')['data']
def getContactListByName(self, name):
if not self.contactList_map:
self._instMap('contactList')
return self.getContactList(self.contactList_map.get(name, 0))
# ===========================================
# Module execution.
#
def main():
module = AnsibleModule(
argument_spec=dict(
account_key=dict(required=True),
account_secret=dict(required=True, no_log=True),
domain=dict(required=True),
sandbox=dict(default='no', type='bool'),
state=dict(required=True, choices=['present', 'absent']),
record_name=dict(required=False),
record_type=dict(required=False, choices=[
'A', 'AAAA', 'CNAME', 'ANAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT']),
record_value=dict(required=False),
record_ttl=dict(required=False, default=1800, type='int'),
monitor=dict(default='no', type='bool'),
systemDescription=dict(default=''),
maxEmails=dict(default=1, type='int'),
protocol=dict(default='HTTP', choices=['TCP', 'UDP', 'HTTP', 'DNS', 'SMTP', 'HTTPS']),
port=dict(default=80, type='int'),
sensitivity=dict(default='Medium', choices=['Low', 'Medium', 'High']),
contactList=dict(default=None),
httpFqdn=dict(required=False),
httpFile=dict(required=False),
httpQueryString=dict(required=False),
failover=dict(default='no', type='bool'),
autoFailover=dict(default='no', type='bool'),
ip1=dict(required=False),
ip2=dict(required=False),
ip3=dict(required=False),
ip4=dict(required=False),
ip5=dict(required=False),
validate_certs=dict(default='yes', type='bool'),
),
required_together=[
['record_value', 'record_ttl', 'record_type']
],
required_if=[
['failover', True, ['autoFailover', 'port', 'protocol', 'ip1', 'ip2']],
['monitor', True, ['port', 'protocol', 'maxEmails', 'systemDescription', 'ip1']]
]
)
protocols = dict(TCP=1, UDP=2, HTTP=3, DNS=4, SMTP=5, HTTPS=6)
sensitivities = dict(Low=8, Medium=5, High=3)
DME = DME2(module.params["account_key"], module.params[
"account_secret"], module.params["domain"], module.params["sandbox"], module)
state = module.params["state"]
record_name = module.params["record_name"]
record_type = module.params["record_type"]
record_value = module.params["record_value"]
# Follow Keyword Controlled Behavior
if record_name is None:
domain_records = DME.getRecords()
if not domain_records:
module.fail_json(
msg="The requested domain name is not accessible with this api_key; try using its ID if known.")
module.exit_json(changed=False, result=domain_records)
# Fetch existing record + Build new one
current_record = DME.getMatchingRecord(record_name, record_type, record_value)
new_record = {'name': record_name}
for i in ["record_value", "record_type", "record_ttl"]:
if not module.params[i] is None:
new_record[i[len("record_"):]] = module.params[i]
# Special handling for mx record
if new_record["type"] == "MX":
new_record["mxLevel"] = new_record["value"].split(" ")[0]
new_record["value"] = new_record["value"].split(" ")[1]
# Special handling for SRV records
if new_record["type"] == "SRV":
new_record["priority"] = new_record["value"].split(" ")[0]
new_record["weight"] = new_record["value"].split(" ")[1]
new_record["port"] = new_record["value"].split(" ")[2]
new_record["value"] = new_record["value"].split(" ")[3]
# Fetch existing monitor if the A record indicates it should exist and build the new monitor
current_monitor = dict()
new_monitor = dict()
if current_record and current_record['type'] == 'A':
current_monitor = DME.getMonitor(current_record['id'])
# Build the new monitor
for i in ['monitor', 'systemDescription', 'protocol', 'port', 'sensitivity', 'maxEmails',
'contactList', 'httpFqdn', 'httpFile', 'httpQueryString',
'failover', 'autoFailover', 'ip1', 'ip2', 'ip3', 'ip4', 'ip5']:
if module.params[i] is not None:
if i == 'protocol':
# The API requires protocol to be a numeric in the range 1-6
new_monitor['protocolId'] = protocols[module.params[i]]
elif i == 'sensitivity':
# The API requires sensitivity to be a numeric of 8, 5, or 3
new_monitor[i] = sensitivities[module.params[i]]
elif i == 'contactList':
# The module accepts either the name or the id of the contact list
contact_list_id = module.params[i]
if not contact_list_id.isdigit() and contact_list_id != '':
contact_list = DME.getContactListByName(contact_list_id)
if not contact_list:
module.fail_json(msg="Contact list {0} does not exist".format(contact_list_id))
contact_list_id = contact_list.get('id', '')
new_monitor['contactListId'] = contact_list_id
else:
# The module option names match the API field names
new_monitor[i] = module.params[i]
# Compare new record against existing one
record_changed = False
if current_record:
for i in new_record:
if str(current_record[i]) != str(new_record[i]):
record_changed = True
new_record['id'] = str(current_record['id'])
monitor_changed = False
if current_monitor:
for i in new_monitor:
if str(current_monitor.get(i)) != str(new_monitor[i]):
monitor_changed = True
# Follow Keyword Controlled Behavior
if state == 'present':
# return the record if no value is specified
if "value" not in new_record:
if not current_record:
module.fail_json(
msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, module.params['domain']))
module.exit_json(changed=False, result=dict(record=current_record, monitor=current_monitor))
# create record and monitor as the record does not exist
if not current_record:
record = DME.createRecord(DME.prepareRecord(new_record))
monitor = DME.updateMonitor(record['id'], DME.prepareMonitor(new_monitor))
module.exit_json(changed=True, result=dict(record=record, monitor=monitor))
# update the record
updated = False
if record_changed:
DME.updateRecord(current_record['id'], DME.prepareRecord(new_record))
updated = True
if monitor_changed:
DME.updateMonitor(current_monitor['recordId'], DME.prepareMonitor(new_monitor))
updated = True
if updated:
module.exit_json(changed=True, result=dict(record=new_record, monitor=new_monitor))
# return the record (no changes)
module.exit_json(changed=False, result=dict(record=current_record, monitor=current_monitor))
elif state == 'absent':
changed = False
# delete the record (and the monitor/failover) if it exists
if current_record:
DME.deleteRecord(current_record['id'])
module.exit_json(changed=True)
# record does not exist, return w/o change.
module.exit_json(changed=changed)
else:
module.fail_json(
msg="'%s' is an unknown value for the state argument" % state)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,210 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: exo_dns_domain
short_description: Manages domain records on Exoscale DNS API.
description:
- Create and remove domain records.
author: "René Moser (@resmo)"
options:
name:
description:
- Name of the record.
required: true
type: str
state:
description:
- State of the resource.
default: present
choices: [ present, absent ]
type: str
extends_documentation_fragment:
- community.general.exoscale
'''
EXAMPLES = '''
- name: Create a domain
exo_dns_domain:
name: example.com
- name: Remove a domain
exo_dns_domain:
name: example.com
state: absent
'''
RETURN = '''
---
exo_dns_domain:
description: API domain results
returned: success
type: complex
contains:
account_id:
description: Your account ID
returned: success
type: int
sample: 34569
auto_renew:
description: Whether domain is auto renewed or not
returned: success
type: bool
sample: false
created_at:
description: When the domain was created
returned: success
type: str
sample: "2016-08-12T15:24:23.989Z"
expires_on:
description: When the domain expires
returned: success
type: str
sample: "2016-08-12T15:24:23.989Z"
id:
description: ID of the domain
returned: success
type: int
sample: "2016-08-12T15:24:23.989Z"
lockable:
description: Whether the domain is lockable or not
returned: success
type: bool
sample: true
name:
description: Domain name
returned: success
type: str
sample: example.com
record_count:
description: Number of records related to this domain
returned: success
type: int
sample: 5
registrant_id:
description: ID of the registrant
returned: success
type: int
sample: null
service_count:
description: Number of services
returned: success
type: int
sample: 0
state:
description: State of the domain
returned: success
type: str
sample: "hosted"
token:
description: Token
returned: success
type: str
sample: "r4NzTRp6opIeFKfaFYvOd6MlhGyD07jl"
unicode_name:
description: Domain name as unicode
returned: success
type: str
sample: "example.com"
updated_at:
description: When the domain was updated last.
returned: success
type: str
sample: "2016-08-12T15:24:23.989Z"
user_id:
description: ID of the user
returned: success
type: int
sample: null
whois_protected:
description: Whether the whois is protected or not
returned: success
type: bool
sample: false
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.exoscale import ExoDns, exo_dns_argument_spec, exo_dns_required_together
class ExoDnsDomain(ExoDns):
def __init__(self, module):
super(ExoDnsDomain, self).__init__(module)
self.name = self.module.params.get('name').lower()
def get_domain(self):
domains = self.api_query("/domains", "GET")
for z in domains:
if z['domain']['name'].lower() == self.name:
return z
return None
def present_domain(self):
domain = self.get_domain()
data = {
'domain': {
'name': self.name,
}
}
if not domain:
self.result['diff']['after'] = data['domain']
self.result['changed'] = True
if not self.module.check_mode:
domain = self.api_query("/domains", "POST", data)
return domain
def absent_domain(self):
domain = self.get_domain()
if domain:
self.result['diff']['before'] = domain
self.result['changed'] = True
if not self.module.check_mode:
self.api_query("/domains/%s" % domain['domain']['name'], "DELETE")
return domain
def get_result(self, resource):
if resource:
self.result['exo_dns_domain'] = resource['domain']
return self.result
def main():
argument_spec = exo_dns_argument_spec()
argument_spec.update(dict(
name=dict(type='str', required=True),
state=dict(type='str', choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=exo_dns_required_together(),
supports_check_mode=True
)
exo_dns_domain = ExoDnsDomain(module)
if module.params.get('state') == "present":
resource = exo_dns_domain.present_domain()
else:
resource = exo_dns_domain.absent_domain()
result = exo_dns_domain.get_result(resource)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,345 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: exo_dns_record
short_description: Manages DNS records on Exoscale DNS.
description:
- Create, update and delete records.
author: "René Moser (@resmo)"
options:
name:
description:
- Name of the record.
default: ""
type: str
domain:
description:
- Domain the record is related to.
required: true
type: str
record_type:
description:
- Type of the record.
default: A
choices: [ A, ALIAS, CNAME, MX, SPF, URL, TXT, NS, SRV, NAPTR, PTR, AAAA, SSHFP, HINFO, POOL ]
aliases: [ rtype, type ]
type: str
content:
description:
- Content of the record.
- Required if C(state=present) or C(multiple=yes).
aliases: [ value, address ]
type: str
ttl:
description:
- TTL of the record in seconds.
default: 3600
type: int
prio:
description:
- Priority of the record.
aliases: [ priority ]
type: int
multiple:
description:
- Whether there are more than one records with similar I(name) and I(record_type).
- Only allowed for a few record types, e.g. C(record_type=A), C(record_type=NS) or C(record_type=MX).
- I(content) will not be updated, instead it is used as a key to find existing records.
type: bool
default: no
state:
description:
- State of the record.
default: present
choices: [ present, absent ]
type: str
extends_documentation_fragment:
- community.general.exoscale
'''
EXAMPLES = '''
- name: Create or update an A record
exo_dns_record:
name: web-vm-1
domain: example.com
content: 1.2.3.4
- name: Update an existing A record with a new IP
exo_dns_record:
name: web-vm-1
domain: example.com
content: 1.2.3.5
- name: Create another A record with same name
exo_dns_record:
name: web-vm-1
domain: example.com
content: 1.2.3.6
multiple: yes
- name: Create or update a CNAME record
exo_dns_record:
name: www
domain: example.com
record_type: CNAME
content: web-vm-1
- name: Create another MX record
exo_dns_record:
domain: example.com
record_type: MX
content: mx1.example.com
prio: 10
multiple: yes
- name: Delete one MX record out of multiple
exo_dns_record:
domain: example.com
record_type: MX
content: mx1.example.com
multiple: yes
state: absent
- name: Remove a single A record
exo_dns_record:
name: www
domain: example.com
state: absent
'''
RETURN = '''
---
exo_dns_record:
description: API record results
returned: success
type: complex
contains:
content:
description: value of the record
returned: success
type: str
sample: 1.2.3.4
created_at:
description: When the record was created
returned: success
type: str
sample: "2016-08-12T15:24:23.989Z"
domain:
description: Name of the domain
returned: success
type: str
sample: example.com
domain_id:
description: ID of the domain
returned: success
type: int
sample: 254324
id:
description: ID of the record
returned: success
type: int
sample: 254324
name:
description: name of the record
returned: success
type: str
sample: www
parent_id:
description: ID of the parent
returned: success
type: int
sample: null
prio:
description: Priority of the record
returned: success
type: int
sample: 10
record_type:
description: Priority of the record
returned: success
type: str
sample: A
system_record:
description: Whether the record is a system record or not
returned: success
type: bool
sample: false
ttl:
description: Time to live of the record
returned: success
type: int
sample: 3600
updated_at:
description: When the record was updated
returned: success
type: str
sample: "2016-08-12T15:24:23.989Z"
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.exoscale import ExoDns, exo_dns_argument_spec, exo_dns_required_together
EXO_RECORD_TYPES = [
'A',
'ALIAS',
'CNAME',
'MX',
'SPF',
'URL',
'TXT',
'NS',
'SRV',
'NAPTR',
'PTR',
'AAAA',
'SSHFP',
'HINFO',
'POOL'
]
class ExoDnsRecord(ExoDns):
def __init__(self, module):
super(ExoDnsRecord, self).__init__(module)
self.domain = self.module.params.get('domain').lower()
self.name = self.module.params.get('name').lower()
if self.name == self.domain:
self.name = ""
self.multiple = self.module.params.get('multiple')
self.record_type = self.module.params.get('record_type')
self.content = self.module.params.get('content')
def _create_record(self, record):
self.result['changed'] = True
data = {
'record': {
'name': self.name,
'record_type': self.record_type,
'content': self.content,
'ttl': self.module.params.get('ttl'),
'prio': self.module.params.get('prio'),
}
}
self.result['diff']['after'] = data['record']
if not self.module.check_mode:
record = self.api_query("/domains/%s/records" % self.domain, "POST", data)
return record
def _update_record(self, record):
data = {
'record': {
'name': self.name,
'content': self.content,
'ttl': self.module.params.get('ttl'),
'prio': self.module.params.get('prio'),
}
}
if self.has_changed(data['record'], record['record']):
self.result['changed'] = True
if not self.module.check_mode:
record = self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "PUT", data)
return record
def get_record(self):
domain = self.module.params.get('domain')
records = self.api_query("/domains/%s/records" % domain, "GET")
result = {}
for r in records:
if r['record']['record_type'] != self.record_type:
continue
r_name = r['record']['name'].lower()
r_content = r['record']['content']
if r_name == self.name:
if not self.multiple:
if result:
self.module.fail_json(msg="More than one record with record_type=%s and name=%s params. "
"Use multiple=yes for more than one record." % (self.record_type, self.name))
else:
result = r
elif r_content == self.content:
return r
return result
def present_record(self):
record = self.get_record()
if not record:
record = self._create_record(record)
else:
record = self._update_record(record)
return record
def absent_record(self):
record = self.get_record()
if record:
self.result['diff']['before'] = record
self.result['changed'] = True
if not self.module.check_mode:
self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "DELETE")
return record
def get_result(self, resource):
if resource:
self.result['exo_dns_record'] = resource['record']
self.result['exo_dns_record']['domain'] = self.domain
return self.result
def main():
argument_spec = exo_dns_argument_spec()
argument_spec.update(dict(
name=dict(type='str', default=''),
record_type=dict(type='str', choices=EXO_RECORD_TYPES, aliases=['rtype', 'type'], default='A'),
content=dict(type='str', aliases=['value', 'address']),
multiple=(dict(type='bool', default=False)),
ttl=dict(type='int', default=3600),
prio=dict(type='int', aliases=['priority']),
domain=dict(type='str', required=True),
state=dict(type='str', choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=exo_dns_required_together(),
required_if=[
('state', 'present', ['content']),
('multiple', True, ['content']),
],
supports_check_mode=True,
)
exo_dns_record = ExoDnsRecord(module)
if module.params.get('state') == "present":
resource = exo_dns_record.present_record()
else:
resource = exo_dns_record.absent_record()
result = exo_dns_record.get_result(resource)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,450 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2014, Ravi Bhure <ravibhure@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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: haproxy
short_description: Enable, disable, and set weights for HAProxy backend servers using socket commands
author:
- Ravi Bhure (@ravibhure)
description:
- Enable, disable, drain and set weights for HAProxy backend servers using socket commands.
notes:
- Enable, disable and drain commands are restricted and can only be issued on
sockets configured for level 'admin'. For example, you can add the line
'stats socket /var/run/haproxy.sock level admin' to the general section of
haproxy.cfg. See U(http://haproxy.1wt.eu/download/1.5/doc/configuration.txt).
- Depends on netcat (nc) being available; you need to install the appropriate
package for your operating system before this module can be used.
options:
backend:
description:
- Name of the HAProxy backend pool.
- If this parameter is unset, it will be auto-detected.
type: str
drain:
description:
- Wait until the server has no active connections or until the timeout
determined by wait_interval and wait_retries is reached.
- Continue only after the status changes to 'MAINT'.
- This overrides the shutdown_sessions option.
type: bool
host:
description:
- Name of the backend host to change.
type: str
required: true
shutdown_sessions:
description:
- When disabling a server, immediately terminate all the sessions attached
to the specified server.
- This can be used to terminate long-running sessions after a server is put
into maintenance mode. Overridden by the drain option.
type: bool
default: no
socket:
description:
- Path to the HAProxy socket file.
type: path
default: /var/run/haproxy.sock
state:
description:
- Desired state of the provided backend host.
- Note that C(drain) state was added in version 2.4.
- It is supported only by HAProxy version 1.5 or later,
- When used on versions < 1.5, it will be ignored.
type: str
required: true
choices: [ disabled, drain, enabled ]
fail_on_not_found:
description:
- Fail whenever trying to enable/disable a backend host that does not exist
type: bool
default: no
wait:
description:
- Wait until the server reports a status of 'UP' when C(state=enabled),
status of 'MAINT' when C(state=disabled) or status of 'DRAIN' when C(state=drain)
type: bool
default: no
wait_interval:
description:
- Number of seconds to wait between retries.
type: int
default: 5
wait_retries:
description:
- Number of times to check for status after changing the state.
type: int
default: 25
weight:
description:
- The value passed in argument.
- If the value ends with the `%` sign, then the new weight will be
relative to the initially configured weight.
- Relative weights are only permitted between 0 and 100% and absolute
weights are permitted between 0 and 256.
type: str
'''
EXAMPLES = r'''
- name: Disable server in 'www' backend pool
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
backend: www
- name: Disable server without backend pool name (apply to all available backend pool)
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
- name: Disable server, provide socket file
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
socket: /var/run/haproxy.sock
backend: www
- name: Disable server, provide socket file, wait until status reports in maintenance
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
socket: /var/run/haproxy.sock
backend: www
wait: yes
# Place server in drain mode, providing a socket file. Then check the server's
# status every minute to see if it changes to maintenance mode, continuing if it
# does in an hour and failing otherwise.
- haproxy:
state: disabled
host: '{{ inventory_hostname }}'
socket: /var/run/haproxy.sock
backend: www
wait: yes
drain: yes
wait_interval: 1
wait_retries: 60
- name: Disable backend server in 'www' backend pool and drop open sessions to it
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
backend: www
socket: /var/run/haproxy.sock
shutdown_sessions: yes
- name: Disable server without backend pool name (apply to all available backend pool) but fail when the backend host is not found
haproxy:
state: disabled
host: '{{ inventory_hostname }}'
fail_on_not_found: yes
- name: Enable server in 'www' backend pool
haproxy:
state: enabled
host: '{{ inventory_hostname }}'
backend: www
- name: Enable server in 'www' backend pool wait until healthy
haproxy:
state: enabled
host: '{{ inventory_hostname }}'
backend: www
wait: yes
- name: Enable server in 'www' backend pool wait until healthy. Retry 10 times with intervals of 5 seconds to retrieve the health
haproxy:
state: enabled
host: '{{ inventory_hostname }}'
backend: www
wait: yes
wait_retries: 10
wait_interval: 5
- name: Enable server in 'www' backend pool with change server(s) weight
haproxy:
state: enabled
host: '{{ inventory_hostname }}'
socket: /var/run/haproxy.sock
weight: 10
backend: www
- name: Set the server in 'www' backend pool to drain mode
haproxy:
state: drain
host: '{{ inventory_hostname }}'
socket: /var/run/haproxy.sock
backend: www
'''
import csv
import socket
import time
from string import Template
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_text
DEFAULT_SOCKET_LOCATION = "/var/run/haproxy.sock"
RECV_SIZE = 1024
ACTION_CHOICES = ['enabled', 'disabled', 'drain']
WAIT_RETRIES = 25
WAIT_INTERVAL = 5
######################################################################
class TimeoutException(Exception):
pass
class HAProxy(object):
"""
Used for communicating with HAProxy through its local UNIX socket interface.
Perform common tasks in Haproxy related to enable server and
disable server.
The complete set of external commands Haproxy handles is documented
on their website:
http://haproxy.1wt.eu/download/1.5/doc/configuration.txt#Unix Socket commands
"""
def __init__(self, module):
self.module = module
self.state = self.module.params['state']
self.host = self.module.params['host']
self.backend = self.module.params['backend']
self.weight = self.module.params['weight']
self.socket = self.module.params['socket']
self.shutdown_sessions = self.module.params['shutdown_sessions']
self.fail_on_not_found = self.module.params['fail_on_not_found']
self.wait = self.module.params['wait']
self.wait_retries = self.module.params['wait_retries']
self.wait_interval = self.module.params['wait_interval']
self._drain = self.module.params['drain']
self.command_results = {}
def execute(self, cmd, timeout=200, capture_output=True):
"""
Executes a HAProxy command by sending a message to a HAProxy's local
UNIX socket and waiting up to 'timeout' milliseconds for the response.
"""
self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.client.connect(self.socket)
self.client.sendall(to_bytes('%s\n' % cmd))
result = b''
buf = b''
buf = self.client.recv(RECV_SIZE)
while buf:
result += buf
buf = self.client.recv(RECV_SIZE)
result = to_text(result, errors='surrogate_or_strict')
if capture_output:
self.capture_command_output(cmd, result.strip())
self.client.close()
return result
def capture_command_output(self, cmd, output):
"""
Capture the output for a command
"""
if 'command' not in self.command_results:
self.command_results['command'] = []
self.command_results['command'].append(cmd)
if 'output' not in self.command_results:
self.command_results['output'] = []
self.command_results['output'].append(output)
def discover_all_backends(self):
"""
Discover all entries with svname = 'BACKEND' and return a list of their corresponding
pxnames
"""
data = self.execute('show stat', 200, False).lstrip('# ')
r = csv.DictReader(data.splitlines())
return tuple(map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r)))
def discover_version(self):
"""
Attempt to extract the haproxy version.
Return a tuple containing major and minor version.
"""
data = self.execute('show info', 200, False)
lines = data.splitlines()
line = [x for x in lines if 'Version:' in x]
try:
version_values = line[0].partition(':')[2].strip().split('.', 3)
version = (int(version_values[0]), int(version_values[1]))
except (ValueError, TypeError, IndexError):
version = None
return version
def execute_for_backends(self, cmd, pxname, svname, wait_for_status=None):
"""
Run some command on the specified backends. If no backends are provided they will
be discovered automatically (all backends)
"""
# Discover backends if none are given
if pxname is None:
backends = self.discover_all_backends()
else:
backends = [pxname]
# Run the command for each requested backend
for backend in backends:
# Fail when backends were not found
state = self.get_state_for(backend, svname)
if (self.fail_on_not_found) and state is None:
self.module.fail_json(
msg="The specified backend '%s/%s' was not found!" % (backend, svname))
if state is not None:
self.execute(Template(cmd).substitute(pxname=backend, svname=svname))
if self.wait:
self.wait_until_status(backend, svname, wait_for_status)
def get_state_for(self, pxname, svname):
"""
Find the state of specific services. When pxname is not set, get all backends for a specific host.
Returns a list of dictionaries containing the status and weight for those services.
"""
data = self.execute('show stat', 200, False).lstrip('# ')
r = csv.DictReader(data.splitlines())
state = tuple(
map(
lambda d: {'status': d['status'], 'weight': d['weight'], 'scur': d['scur']},
filter(lambda d: (pxname is None or d['pxname']
== pxname) and d['svname'] == svname, r)
)
)
return state or None
def wait_until_status(self, pxname, svname, status):
"""
Wait for a service to reach the specified status. Try RETRIES times
with INTERVAL seconds of sleep in between. If the service has not reached
the expected status in that time, the module will fail. If the service was
not found, the module will fail.
"""
for i in range(1, self.wait_retries):
state = self.get_state_for(pxname, svname)
# We can assume there will only be 1 element in state because both svname and pxname are always set when we get here
# When using track we get a status like this: MAINT (via pxname/svname) so we need to do substring matching
if status in state[0]['status']:
if not self._drain or (state[0]['scur'] == '0' and 'MAINT' in state):
return True
else:
time.sleep(self.wait_interval)
self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." %
(pxname, svname, status, self.wait_retries))
def enabled(self, host, backend, weight):
"""
Enabled action, marks server to UP and checks are re-enabled,
also supports to get current weight for server (default) and
set the weight for haproxy backend server when provides.
"""
cmd = "get weight $pxname/$svname; enable server $pxname/$svname"
if weight:
cmd += "; set weight $pxname/$svname %s" % weight
self.execute_for_backends(cmd, backend, host, 'UP')
def disabled(self, host, backend, shutdown_sessions):
"""
Disabled action, marks server to DOWN for maintenance. In this mode, no more checks will be
performed on the server until it leaves maintenance,
also it shutdown sessions while disabling backend host server.
"""
cmd = "get weight $pxname/$svname; disable server $pxname/$svname"
if shutdown_sessions:
cmd += "; shutdown sessions server $pxname/$svname"
self.execute_for_backends(cmd, backend, host, 'MAINT')
def drain(self, host, backend, status='DRAIN'):
"""
Drain action, sets the server to DRAIN mode.
In this mode mode, the server will not accept any new connections
other than those that are accepted via persistence.
"""
haproxy_version = self.discover_version()
# check if haproxy version suppots DRAIN state (starting with 1.5)
if haproxy_version and (1, 5) <= haproxy_version:
cmd = "set server $pxname/$svname state drain"
self.execute_for_backends(cmd, backend, host, status)
def act(self):
"""
Figure out what you want to do from ansible, and then do it.
"""
# Get the state before the run
self.command_results['state_before'] = self.get_state_for(self.backend, self.host)
# toggle enable/disbale server
if self.state == 'enabled':
self.enabled(self.host, self.backend, self.weight)
elif self.state == 'disabled' and self._drain:
self.drain(self.host, self.backend, status='MAINT')
elif self.state == 'disabled':
self.disabled(self.host, self.backend, self.shutdown_sessions)
elif self.state == 'drain':
self.drain(self.host, self.backend)
else:
self.module.fail_json(msg="unknown state specified: '%s'" % self.state)
# Get the state after the run
self.command_results['state_after'] = self.get_state_for(self.backend, self.host)
# Report change status
self.command_results['changed'] = (self.command_results['state_before'] != self.command_results['state_after'])
self.module.exit_json(**self.command_results)
def main():
# load ansible module object
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', required=True, choices=ACTION_CHOICES),
host=dict(type='str', required=True),
backend=dict(type='str'),
weight=dict(type='str'),
socket=dict(type='path', default=DEFAULT_SOCKET_LOCATION),
shutdown_sessions=dict(type='bool', default=False),
fail_on_not_found=dict(type='bool', default=False),
wait=dict(type='bool', default=False),
wait_retries=dict(type='int', default=WAIT_RETRIES),
wait_interval=dict(type='int', default=WAIT_INTERVAL),
drain=dict(type='bool', default=False),
),
)
if not socket:
module.fail_json(msg="unable to locate haproxy socket")
ansible_haproxy = HAProxy(module)
ansible_haproxy.act()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,146 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2019 Felix Fontein <felix@fontein.de>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: hetzner_failover_ip
short_description: Manage Hetzner's failover IPs
author:
- Felix Fontein (@felixfontein)
description:
- Manage Hetzner's failover IPs.
seealso:
- name: Failover IP documentation
description: Hetzner's documentation on failover IPs.
link: https://wiki.hetzner.de/index.php/Failover/en
- module: hetzner_failover_ip_info
description: Retrieve information on failover IPs.
extends_documentation_fragment:
- community.general.hetzner
options:
failover_ip:
description: The failover IP address.
type: str
required: yes
state:
description:
- Defines whether the IP will be routed or not.
- If set to C(routed), I(value) must be specified.
type: str
choices:
- routed
- unrouted
default: routed
value:
description:
- The new value for the failover IP address.
- Required when setting I(state) to C(routed).
type: str
timeout:
description:
- Timeout to use when routing or unrouting the failover IP.
- Note that the API call returns when the failover IP has been
successfully routed to the new address, respectively successfully
unrouted.
type: int
default: 180
'''
EXAMPLES = r'''
- name: Set value of failover IP 1.2.3.4 to 5.6.7.8
hetzner_failover_ip:
hetzner_user: foo
hetzner_password: bar
failover_ip: 1.2.3.4
value: 5.6.7.8
- name: Set value of failover IP 1.2.3.4 to unrouted
hetzner_failover_ip:
hetzner_user: foo
hetzner_password: bar
failover_ip: 1.2.3.4
state: unrouted
'''
RETURN = r'''
value:
description:
- The value of the failover IP.
- Will be C(none) if the IP is unrouted.
returned: success
type: str
state:
description:
- Will be C(routed) or C(unrouted).
returned: success
type: str
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.hetzner import (
HETZNER_DEFAULT_ARGUMENT_SPEC,
get_failover,
set_failover,
get_failover_state,
)
def main():
argument_spec = dict(
failover_ip=dict(type='str', required=True),
state=dict(type='str', default='routed', choices=['routed', 'unrouted']),
value=dict(type='str'),
timeout=dict(type='int', default=180),
)
argument_spec.update(HETZNER_DEFAULT_ARGUMENT_SPEC)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=(
('state', 'routed', ['value']),
),
)
failover_ip = module.params['failover_ip']
value = get_failover(module, failover_ip)
changed = False
before = get_failover_state(value)
if module.params['state'] == 'routed':
new_value = module.params['value']
else:
new_value = None
if value != new_value:
if module.check_mode:
value = new_value
changed = True
else:
value, changed = set_failover(module, failover_ip, new_value, timeout=module.params['timeout'])
after = get_failover_state(value)
module.exit_json(
changed=changed,
diff=dict(
before=before,
after=after,
),
**after
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,122 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2019 Felix Fontein <felix@fontein.de>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: hetzner_failover_ip_info
short_description: Retrieve information on Hetzner's failover IPs
author:
- Felix Fontein (@felixfontein)
description:
- Retrieve information on Hetzner's failover IPs.
seealso:
- name: Failover IP documentation
description: Hetzner's documentation on failover IPs.
link: https://wiki.hetzner.de/index.php/Failover/en
- module: hetzner_failover_ip
description: Manage failover IPs.
extends_documentation_fragment:
- community.general.hetzner
options:
failover_ip:
description: The failover IP address.
type: str
required: yes
'''
EXAMPLES = r'''
- name: Get value of failover IP 1.2.3.4
hetzner_failover_ip_info:
hetzner_user: foo
hetzner_password: bar
failover_ip: 1.2.3.4
value: 5.6.7.8
register: result
- name: Print value of failover IP 1.2.3.4 in case it is routed
debug:
msg: "1.2.3.4 routes to {{ result.value }}"
when: result.state == 'routed'
'''
RETURN = r'''
value:
description:
- The value of the failover IP.
- Will be C(none) if the IP is unrouted.
returned: success
type: str
state:
description:
- Will be C(routed) or C(unrouted).
returned: success
type: str
failover_ip:
description:
- The failover IP.
returned: success
type: str
sample: '1.2.3.4'
failover_netmask:
description:
- The netmask for the failover IP.
returned: success
type: str
sample: '255.255.255.255'
server_ip:
description:
- The main IP of the server this failover IP is associated to.
- This is I(not) the server the failover IP is routed to.
returned: success
type: str
server_number:
description:
- The number of the server this failover IP is associated to.
- This is I(not) the server the failover IP is routed to.
returned: success
type: int
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.hetzner import (
HETZNER_DEFAULT_ARGUMENT_SPEC,
get_failover_record,
get_failover_state,
)
def main():
argument_spec = dict(
failover_ip=dict(type='str', required=True),
)
argument_spec.update(HETZNER_DEFAULT_ARGUMENT_SPEC)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
failover = get_failover_record(module, module.params['failover_ip'])
result = get_failover_state(failover['active_server_ip'])
result['failover_ip'] = failover['ip']
result['failover_netmask'] = failover['netmask']
result['server_ip'] = failover['server_ip']
result['server_number'] = failover['server_number']
result['changed'] = False
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,513 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2019 Felix Fontein <felix@fontein.de>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: hetzner_firewall
short_description: Manage Hetzner's dedicated server firewall
author:
- Felix Fontein (@felixfontein)
description:
- Manage Hetzner's dedicated server firewall.
- Note that idempotency check for TCP flags simply compares strings and doesn't
try to interpret the rules. This might change in the future.
seealso:
- name: Firewall documentation
description: Hetzner's documentation on the stateless firewall for dedicated servers
link: https://wiki.hetzner.de/index.php/Robot_Firewall/en
- module: hetzner_firewall_info
description: Retrieve information on firewall configuration.
extends_documentation_fragment:
- community.general.hetzner
options:
server_ip:
description: The server's main IP address.
required: yes
type: str
port:
description:
- Switch port of firewall.
type: str
choices: [ main, kvm ]
default: main
state:
description:
- Status of the firewall.
- Firewall is active if state is C(present), and disabled if state is C(absent).
type: str
default: present
choices: [ present, absent ]
whitelist_hos:
description:
- Whether Hetzner services have access.
type: bool
rules:
description:
- Firewall rules.
type: dict
suboptions:
input:
description:
- Input firewall rules.
type: list
elements: dict
suboptions:
name:
description:
- Name of the firewall rule.
type: str
ip_version:
description:
- Internet protocol version.
- Note that currently, only IPv4 is supported by Hetzner.
required: yes
type: str
choices: [ ipv4, ipv6 ]
dst_ip:
description:
- Destination IP address or subnet address.
- CIDR notation.
type: str
dst_port:
description:
- Destination port or port range.
type: str
src_ip:
description:
- Source IP address or subnet address.
- CIDR notation.
type: str
src_port:
description:
- Source port or port range.
type: str
protocol:
description:
- Protocol above IP layer
type: str
tcp_flags:
description:
- TCP flags or logical combination of flags.
- Flags supported by Hetzner are C(syn), C(fin), C(rst), C(psh) and C(urg).
- They can be combined with C(|) (logical or) and C(&) (logical and).
- See L(the documentation,https://wiki.hetzner.de/index.php/Robot_Firewall/en#Parameter)
for more information.
type: str
action:
description:
- Action if rule matches.
required: yes
type: str
choices: [ accept, discard ]
update_timeout:
description:
- Timeout to use when configuring the firewall.
- Note that the API call returns before the firewall has been
successfully set up.
type: int
default: 30
wait_for_configured:
description:
- Whether to wait until the firewall has been successfully configured before
determining what to do, and before returning from the module.
- The API returns status C(in progress) when the firewall is currently
being configured. If this happens, the module will try again until
the status changes to C(active) or C(disabled).
- Please note that there is a request limit. If you have to do multiple
updates, it can be better to disable waiting, and regularly use
M(hetzner_firewall_info) to query status.
type: bool
default: yes
wait_delay:
description:
- Delay to wait (in seconds) before checking again whether the firewall has
been configured.
type: int
default: 10
timeout:
description:
- Timeout (in seconds) for waiting for firewall to be configured.
type: int
default: 180
'''
EXAMPLES = r'''
- name: Configure firewall for server with main IP 1.2.3.4
hetzner_firewall:
hetzner_user: foo
hetzner_password: bar
server_ip: 1.2.3.4
status: active
whitelist_hos: yes
rules:
input:
- name: Allow everything to ports 20-23 from 4.3.2.1/24
ip_version: ipv4
src_ip: 4.3.2.1/24
dst_port: '20-23'
action: accept
- name: Allow everything to port 443
ip_version: ipv4
dst_port: '443'
action: accept
- name: Drop everything else
ip_version: ipv4
action: discard
register: result
- debug:
msg: "{{ result }}"
'''
RETURN = r'''
firewall:
description:
- The firewall configuration.
type: dict
returned: success
contains:
port:
description:
- Switch port of firewall.
- C(main) or C(kvm).
type: str
sample: main
server_ip:
description:
- Server's main IP address.
type: str
sample: 1.2.3.4
server_number:
description:
- Hetzner's internal server number.
type: int
sample: 12345
status:
description:
- Status of the firewall.
- C(active) or C(disabled).
- Will be C(in process) if the firewall is currently updated, and
I(wait_for_configured) is set to C(no) or I(timeout) to a too small value.
type: str
sample: active
whitelist_hos:
description:
- Whether Hetzner services have access.
type: bool
sample: true
rules:
description:
- Firewall rules.
type: dict
contains:
input:
description:
- Input firewall rules.
type: list
elements: dict
contains:
name:
description:
- Name of the firewall rule.
type: str
sample: Allow HTTP access to server
ip_version:
description:
- Internet protocol version.
type: str
sample: ipv4
dst_ip:
description:
- Destination IP address or subnet address.
- CIDR notation.
type: str
sample: 1.2.3.4/32
dst_port:
description:
- Destination port or port range.
type: str
sample: "443"
src_ip:
description:
- Source IP address or subnet address.
- CIDR notation.
type: str
sample: null
src_port:
description:
- Source port or port range.
type: str
sample: null
protocol:
description:
- Protocol above IP layer
type: str
sample: tcp
tcp_flags:
description:
- TCP flags or logical combination of flags.
type: str
sample: null
action:
description:
- Action if rule matches.
- C(accept) or C(discard).
type: str
sample: accept
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress
from ansible_collections.community.general.plugins.module_utils.hetzner import (
HETZNER_DEFAULT_ARGUMENT_SPEC,
BASE_URL,
fetch_url_json,
fetch_url_json_with_retries,
CheckDoneTimeoutException,
)
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils._text import to_native, to_text
RULE_OPTION_NAMES = [
'name', 'ip_version', 'dst_ip', 'dst_port', 'src_ip', 'src_port',
'protocol', 'tcp_flags', 'action',
]
RULES = ['input']
def restrict_dict(dictionary, fields):
result = dict()
for k, v in dictionary.items():
if k in fields:
result[k] = v
return result
def restrict_firewall_config(config):
result = restrict_dict(config, ['port', 'status', 'whitelist_hos'])
result['rules'] = dict()
for ruleset in RULES:
result['rules'][ruleset] = [
restrict_dict(rule, RULE_OPTION_NAMES)
for rule in config['rules'].get(ruleset) or []
]
return result
def update(before, after, params, name):
bv = before.get(name)
after[name] = bv
changed = False
pv = params[name]
if pv is not None:
changed = pv != bv
if changed:
after[name] = pv
return changed
def normalize_ip(ip, ip_version):
if ip is None:
return ip
if '/' in ip:
ip, range = ip.split('/')
else:
ip, range = ip, ''
ip_addr = to_native(compat_ipaddress.ip_address(to_text(ip)).compressed)
if range == '':
range = '32' if ip_version.lower() == 'ipv4' else '128'
return ip_addr + '/' + range
def update_rules(before, after, params, ruleset):
before_rules = before['rules'][ruleset]
after_rules = after['rules'][ruleset]
params_rules = params['rules'][ruleset]
changed = len(before_rules) != len(params_rules)
for no, rule in enumerate(params_rules):
rule['src_ip'] = normalize_ip(rule['src_ip'], rule['ip_version'])
rule['dst_ip'] = normalize_ip(rule['dst_ip'], rule['ip_version'])
if no < len(before_rules):
before_rule = before_rules[no]
before_rule['src_ip'] = normalize_ip(before_rule['src_ip'], before_rule['ip_version'])
before_rule['dst_ip'] = normalize_ip(before_rule['dst_ip'], before_rule['ip_version'])
if before_rule != rule:
changed = True
after_rules.append(rule)
return changed
def encode_rule(output, rulename, input):
for i, rule in enumerate(input['rules'][rulename]):
for k, v in rule.items():
if v is not None:
output['rules[{0}][{1}][{2}]'.format(rulename, i, k)] = v
def create_default_rules_object():
rules = dict()
for ruleset in RULES:
rules[ruleset] = []
return rules
def firewall_configured(result, error):
return result['firewall']['status'] != 'in process'
def main():
argument_spec = dict(
server_ip=dict(type='str', required=True),
port=dict(type='str', default='main', choices=['main', 'kvm']),
state=dict(type='str', default='present', choices=['present', 'absent']),
whitelist_hos=dict(type='bool'),
rules=dict(type='dict', options=dict(
input=dict(type='list', elements='dict', options=dict(
name=dict(type='str'),
ip_version=dict(type='str', required=True, choices=['ipv4', 'ipv6']),
dst_ip=dict(type='str'),
dst_port=dict(type='str'),
src_ip=dict(type='str'),
src_port=dict(type='str'),
protocol=dict(type='str'),
tcp_flags=dict(type='str'),
action=dict(type='str', required=True, choices=['accept', 'discard']),
)),
)),
update_timeout=dict(type='int', default=30),
wait_for_configured=dict(type='bool', default=True),
wait_delay=dict(type='int', default=10),
timeout=dict(type='int', default=180),
)
argument_spec.update(HETZNER_DEFAULT_ARGUMENT_SPEC)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
# Sanitize input
module.params['status'] = 'active' if (module.params['state'] == 'present') else 'disabled'
if module.params['rules'] is None:
module.params['rules'] = {}
if module.params['rules'].get('input') is None:
module.params['rules']['input'] = []
server_ip = module.params['server_ip']
# https://robot.your-server.de/doc/webservice/en.html#get-firewall-server-ip
url = "{0}/firewall/{1}".format(BASE_URL, server_ip)
if module.params['wait_for_configured']:
try:
result, error = fetch_url_json_with_retries(
module,
url,
check_done_callback=firewall_configured,
check_done_delay=module.params['wait_delay'],
check_done_timeout=module.params['timeout'],
)
except CheckDoneTimeoutException as dummy:
module.fail_json(msg='Timeout while waiting for firewall to be configured.')
else:
result, error = fetch_url_json(module, url)
if not firewall_configured(result, error):
module.fail_json(msg='Firewall configuration cannot be read as it is not configured.')
full_before = result['firewall']
if not full_before.get('rules'):
full_before['rules'] = create_default_rules_object()
before = restrict_firewall_config(full_before)
# Build wanted (after) state and compare
after = dict(before)
changed = False
changed |= update(before, after, module.params, 'port')
changed |= update(before, after, module.params, 'status')
changed |= update(before, after, module.params, 'whitelist_hos')
after['rules'] = create_default_rules_object()
if module.params['status'] == 'active':
for ruleset in RULES:
changed |= update_rules(before, after, module.params, ruleset)
# Update if different
construct_result = True
construct_status = None
if changed and not module.check_mode:
# https://robot.your-server.de/doc/webservice/en.html#post-firewall-server-ip
url = "{0}/firewall/{1}".format(BASE_URL, server_ip)
headers = {"Content-type": "application/x-www-form-urlencoded"}
data = dict(after)
data['whitelist_hos'] = str(data['whitelist_hos']).lower()
del data['rules']
for ruleset in RULES:
encode_rule(data, ruleset, after)
result, error = fetch_url_json(
module,
url,
method='POST',
timeout=module.params['update_timeout'],
data=urlencode(data),
headers=headers,
)
if module.params['wait_for_configured'] and not firewall_configured(result, error):
try:
result, error = fetch_url_json_with_retries(
module,
url,
check_done_callback=firewall_configured,
check_done_delay=module.params['wait_delay'],
check_done_timeout=module.params['timeout'],
skip_first=True,
)
except CheckDoneTimeoutException as e:
result, error = e.result, e.error
module.warn('Timeout while waiting for firewall to be configured.')
full_after = result['firewall']
if not full_after.get('rules'):
full_after['rules'] = create_default_rules_object()
construct_status = full_after['status']
if construct_status != 'in process':
# Only use result if configuration is done, so that diff will be ok
after = restrict_firewall_config(full_after)
construct_result = False
if construct_result:
# Construct result (used for check mode, and configuration still in process)
full_after = dict(full_before)
for k, v in after.items():
if k != 'rules':
full_after[k] = after[k]
if construct_status is not None:
# We want 'in process' here
full_after['status'] = construct_status
full_after['rules'] = dict()
for ruleset in RULES:
full_after['rules'][ruleset] = after['rules'][ruleset]
module.exit_json(
changed=changed,
diff=dict(
before=before,
after=after,
),
firewall=full_after,
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,230 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2019 Felix Fontein <felix@fontein.de>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: hetzner_firewall_info
short_description: Manage Hetzner's dedicated server firewall
author:
- Felix Fontein (@felixfontein)
description:
- Manage Hetzner's dedicated server firewall.
seealso:
- name: Firewall documentation
description: Hetzner's documentation on the stateless firewall for dedicated servers
link: https://wiki.hetzner.de/index.php/Robot_Firewall/en
- module: hetzner_firewall
description: Configure firewall.
extends_documentation_fragment:
- community.general.hetzner
options:
server_ip:
description: The server's main IP address.
type: str
required: yes
wait_for_configured:
description:
- Whether to wait until the firewall has been successfully configured before
determining what to do, and before returning from the module.
- The API returns status C(in progress) when the firewall is currently
being configured. If this happens, the module will try again until
the status changes to C(active) or C(disabled).
- Please note that there is a request limit. If you have to do multiple
updates, it can be better to disable waiting, and regularly use
M(hetzner_firewall_info) to query status.
type: bool
default: yes
wait_delay:
description:
- Delay to wait (in seconds) before checking again whether the firewall has
been configured.
type: int
default: 10
timeout:
description:
- Timeout (in seconds) for waiting for firewall to be configured.
type: int
default: 180
'''
EXAMPLES = r'''
- name: Get firewall configuration for server with main IP 1.2.3.4
hetzner_firewall_info:
hetzner_user: foo
hetzner_password: bar
server_ip: 1.2.3.4
register: result
- debug:
msg: "{{ result.firewall }}"
'''
RETURN = r'''
firewall:
description:
- The firewall configuration.
type: dict
returned: success
contains:
port:
description:
- Switch port of firewall.
- C(main) or C(kvm).
type: str
sample: main
server_ip:
description:
- Server's main IP address.
type: str
sample: 1.2.3.4
server_number:
description:
- Hetzner's internal server number.
type: int
sample: 12345
status:
description:
- Status of the firewall.
- C(active) or C(disabled).
- Will be C(in process) if the firewall is currently updated, and
I(wait_for_configured) is set to C(no) or I(timeout) to a too small value.
type: str
sample: active
whitelist_hos:
description:
- Whether Hetzner services have access.
type: bool
sample: true
rules:
description:
- Firewall rules.
type: dict
contains:
input:
description:
- Input firewall rules.
type: list
elements: dict
contains:
name:
description:
- Name of the firewall rule.
type: str
sample: Allow HTTP access to server
ip_version:
description:
- Internet protocol version.
type: str
sample: ipv4
dst_ip:
description:
- Destination IP address or subnet address.
- CIDR notation.
type: str
sample: 1.2.3.4/32
dst_port:
description:
- Destination port or port range.
type: str
sample: "443"
src_ip:
description:
- Source IP address or subnet address.
- CIDR notation.
type: str
sample: null
src_port:
description:
- Source port or port range.
type: str
sample: null
protocol:
description:
- Protocol above IP layer
type: str
sample: tcp
tcp_flags:
description:
- TCP flags or logical combination of flags.
type: str
sample: null
action:
description:
- Action if rule matches.
- C(accept) or C(discard).
type: str
sample: accept
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.hetzner import (
HETZNER_DEFAULT_ARGUMENT_SPEC,
BASE_URL,
fetch_url_json,
fetch_url_json_with_retries,
CheckDoneTimeoutException,
)
def firewall_configured(result, error):
return result['firewall']['status'] != 'in process'
def main():
argument_spec = dict(
server_ip=dict(type='str', required=True),
wait_for_configured=dict(type='bool', default=True),
wait_delay=dict(type='int', default=10),
timeout=dict(type='int', default=180),
)
argument_spec.update(HETZNER_DEFAULT_ARGUMENT_SPEC)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
server_ip = module.params['server_ip']
# https://robot.your-server.de/doc/webservice/en.html#get-firewall-server-ip
url = "{0}/firewall/{1}".format(BASE_URL, server_ip)
if module.params['wait_for_configured']:
try:
result, error = fetch_url_json_with_retries(
module,
url,
check_done_callback=firewall_configured,
check_done_delay=module.params['wait_delay'],
check_done_timeout=module.params['timeout'],
)
except CheckDoneTimeoutException as dummy:
module.fail_json(msg='Timeout while waiting for firewall to be configured.')
else:
result, error = fetch_url_json(module, url)
firewall = result['firewall']
if not firewall.get('rules'):
firewall['rules'] = dict()
for ruleset in ['input']:
firewall['rules'][ruleset] = []
module.exit_json(
changed=False,
firewall=firewall,
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,569 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, <meiliu@fusionlayer.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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
module: infinity
short_description: Manage Infinity IPAM using Rest API
description:
- Manage Infinity IPAM using REST API.
author:
- Meirong Liu (@MeganLiu)
options:
server_ip:
description:
- Infinity server_ip with IP address.
type: str
required: true
username:
description:
- Username to access Infinity.
- The user must have REST API privileges.
type: str
required: true
password:
description:
- Infinity password.
type: str
required: true
action:
description:
- Action to perform
type: str
required: true
choices: [add_network, delete_network, get_network, get_network_id, release_ip, release_network, reserve_network, reserve_next_available_ip ]
network_id:
description:
- Network ID.
type: str
default: ''
ip_address:
description:
- IP Address for a reservation or a release.
type: str
default: ''
network_address:
description:
- Network address with CIDR format (e.g., 192.168.310.0).
type: str
default: ''
network_size:
description:
- Network bitmask (e.g. 255.255.255.220) or CIDR format (e.g., /26).
type: str
default: ''
network_name:
description:
- The name of a network.
type: str
default: ''
network_location:
description:
- The parent network id for a given network.
type: int
default: -1
network_type:
description:
- Network type defined by Infinity
type: str
choices: [ lan, shared_lan, supernet ]
default: lan
network_family:
description:
- Network family defined by Infinity, e.g. IPv4, IPv6 and Dual stack
type: str
choices: [ 4, 6, dual ]
default: 4
'''
EXAMPLES = r'''
---
- hosts: localhost
connection: local
strategy: debug
tasks:
- name: Reserve network into Infinity IPAM
infinity:
server_ip: 80.75.107.12
username: username
password: password
action: reserve_network
network_name: reserve_new_ansible_network
network_family: 4
network_type: lan
network_id: 1201
network_size: /28
register: infinity
'''
RETURN = r'''
network_id:
description: id for a given network
returned: success
type: str
sample: '1501'
ip_info:
description: when reserve next available ip address from a network, the ip address info ) is returned.
returned: success
type: str
sample: '{"address": "192.168.10.3", "hostname": "", "FQDN": "", "domainname": "", "id": 3229}'
network_info:
description: when reserving a LAN network from a Infinity supernet by providing network_size, the information about the reserved network is returned.
returned: success
type: str
sample: {"network_address": "192.168.10.32/28","network_family": "4", "network_id": 3102,
"network_size": null,"description": null,"network_location": "3085",
"ranges": { "id": 0, "name": null,"first_ip": null,"type": null,"last_ip": null},
"network_type": "lan","network_name": "'reserve_new_ansible_network'"}
'''
from ansible.module_utils.basic import AnsibleModule, json
from ansible.module_utils.urls import open_url
class Infinity(object):
"""
Class for manage REST API calls with the Infinity.
"""
def __init__(self, module, server_ip, username, password):
self.module = module
self.auth_user = username
self.auth_pass = password
self.base_url = "https://%s/rest/v1/" % (str(server_ip))
def _get_api_call_ansible_handler(
self,
method='get',
resource_url='',
stat_codes=None,
params=None,
payload_data=None):
"""
Perform the HTTPS request by using ansible get/delete method
"""
stat_codes = [200] if stat_codes is None else stat_codes
request_url = str(self.base_url) + str(resource_url)
response = None
headers = {'Content-Type': 'application/json'}
if not request_url:
self.module.exit_json(
msg="When sending Rest api call , the resource URL is empty, please check.")
if payload_data and not isinstance(payload_data, str):
payload_data = json.dumps(payload_data)
response_raw = open_url(
str(request_url),
method=method,
timeout=20,
headers=headers,
url_username=self.auth_user,
url_password=self.auth_pass,
validate_certs=False,
force_basic_auth=True,
data=payload_data)
response = response_raw.read()
payload = ''
if response_raw.code not in stat_codes:
self.module.exit_json(
changed=False,
meta=" openurl response_raw.code show error and error code is %r" %
(response_raw.code))
else:
if isinstance(response, str) and len(response) > 0:
payload = response
elif method.lower() == 'delete' and response_raw.code == 204:
payload = 'Delete is done.'
if isinstance(payload, dict) and "text" in payload:
self.module.exit_json(
changed=False,
meta="when calling rest api, returned data is not json ")
raise Exception(payload["text"])
return payload
# ---------------------------------------------------------------------------
# get_network()
# ---------------------------------------------------------------------------
def get_network(self, network_id, network_name, limit=-1):
"""
Search network_name inside Infinity by using rest api
Network id or network_name needs to be provided
return the details of a given with given network_id or name
"""
if network_name is None and network_id is None:
self.module.exit_json(
msg="You must specify one of the options 'network_name' or 'network_id'.")
method = "get"
resource_url = ''
params = {}
response = None
if network_id:
resource_url = "networks/" + str(network_id)
response = self._get_api_call_ansible_handler(method, resource_url)
if network_id is None and network_name:
method = "get"
resource_url = "search"
params = {"query": json.dumps(
{"name": network_name, "type": "network"})}
response = self._get_api_call_ansible_handler(
method, resource_url, payload_data=json.dumps(params))
if response and isinstance(response, str):
response = json.loads(response)
if response and isinstance(response, list) and len(
response) > 1 and limit == 1:
response = response[0]
response = json.dumps(response)
return response
# ---------------------------------------------------------------------------
# get_network_id()
# ---------------------------------------------------------------------------
def get_network_id(self, network_name="", network_type='lan'):
"""
query network_id from Infinity via rest api based on given network_name
"""
method = 'get'
resource_url = 'search'
response = None
if network_name is None:
self.module.exit_json(
msg="You must specify the option 'network_name'")
params = {"query": json.dumps(
{"name": network_name, "type": "network"})}
response = self._get_api_call_ansible_handler(
method, resource_url, payload_data=json.dumps(params))
network_id = ""
if response and isinstance(response, str):
response = json.loads(response)
if response and isinstance(response, list):
response = response[0]
network_id = response['id']
return network_id
# ---------------------------------------------------------------------------
# reserve_next_available_ip()
# ---------------------------------------------------------------------------
def reserve_next_available_ip(self, network_id=""):
"""
Reserve ip address via Infinity by using rest api
network_id: the id of the network that users would like to reserve network from
return the next available ip address from that given network
"""
method = "post"
resource_url = ''
response = None
ip_info = ''
if not network_id:
self.module.exit_json(
msg="You must specify the option 'network_id'.")
if network_id:
resource_url = "networks/" + str(network_id) + "/reserve_ip"
response = self._get_api_call_ansible_handler(method, resource_url)
if response and response.find(
"[") >= 0 and response.find("]") >= 0:
start_pos = response.find("{")
end_pos = response.find("}")
ip_info = response[start_pos: (end_pos + 1)]
return ip_info
# -------------------------
# release_ip()
# -------------------------
def release_ip(self, network_id="", ip_address=""):
"""
Reserve ip address via Infinity by using rest api
"""
method = "get"
resource_url = ''
response = None
if ip_address is None or network_id is None:
self.module.exit_json(
msg="You must specify those two options: 'network_id' and 'ip_address'.")
resource_url = "networks/" + str(network_id) + "/children"
response = self._get_api_call_ansible_handler(method, resource_url)
if not response:
self.module.exit_json(
msg="There is an error in release ip %s from network %s." %
(ip_address, network_id))
ip_list = json.loads(response)
ip_idlist = []
for ip_item in ip_list:
ip_id = ip_item['id']
ip_idlist.append(ip_id)
deleted_ip_id = ''
for ip_id in ip_idlist:
ip_response = ''
resource_url = "ip_addresses/" + str(ip_id)
ip_response = self._get_api_call_ansible_handler(
method,
resource_url,
stat_codes=[200])
if ip_response and json.loads(
ip_response)['address'] == str(ip_address):
deleted_ip_id = ip_id
break
if deleted_ip_id:
method = 'delete'
resource_url = "ip_addresses/" + str(deleted_ip_id)
response = self._get_api_call_ansible_handler(
method, resource_url, stat_codes=[204])
else:
self.module.exit_json(
msg=" When release ip, could not find the ip address %r from the given network %r' ." %
(ip_address, network_id))
return response
# -------------------
# delete_network()
# -------------------
def delete_network(self, network_id="", network_name=""):
"""
delete network from Infinity by using rest api
"""
method = 'delete'
resource_url = ''
response = None
if network_id is None and network_name is None:
self.module.exit_json(
msg="You must specify one of those options: 'network_id','network_name' .")
if network_id is None and network_name:
network_id = self.get_network_id(network_name=network_name)
if network_id:
resource_url = "networks/" + str(network_id)
response = self._get_api_call_ansible_handler(
method, resource_url, stat_codes=[204])
return response
# reserve_network()
# ---------------------------------------------------------------------------
def reserve_network(self, network_id="",
reserved_network_name="", reserved_network_description="",
reserved_network_size="", reserved_network_family='4',
reserved_network_type='lan', reserved_network_address="",):
"""
Reserves the first available network of specified size from a given supernet
<dt>network_name (required)</dt><dd>Name of the network</dd>
<dt>description (optional)</dt><dd>Free description</dd>
<dt>network_family (required)</dt><dd>Address family of the network. One of '4', '6', 'IPv4', 'IPv6', 'dual'</dd>
<dt>network_address (optional)</dt><dd>Address of the new network. If not given, the first network available will be created.</dd>
<dt>network_size (required)</dt><dd>Size of the new network in /&lt;prefix&gt; notation.</dd>
<dt>network_type (required)</dt><dd>Type of network. One of 'supernet', 'lan', 'shared_lan'</dd>
"""
method = 'post'
resource_url = ''
network_info = None
if network_id is None or reserved_network_name is None or reserved_network_size is None:
self.module.exit_json(
msg="You must specify those options: 'network_id', 'reserved_network_name' and 'reserved_network_size'")
if network_id:
resource_url = "networks/" + str(network_id) + "/reserve_network"
if not reserved_network_family:
reserved_network_family = '4'
if not reserved_network_type:
reserved_network_type = 'lan'
payload_data = {
"network_name": reserved_network_name,
'description': reserved_network_description,
'network_size': reserved_network_size,
'network_family': reserved_network_family,
'network_type': reserved_network_type,
'network_location': int(network_id)}
if reserved_network_address:
payload_data.update({'network_address': reserved_network_address})
network_info = self._get_api_call_ansible_handler(
method, resource_url, stat_codes=[200, 201], payload_data=payload_data)
return network_info
# ---------------------------------------------------------------------------
# release_network()
# ---------------------------------------------------------------------------
def release_network(
self,
network_id="",
released_network_name="",
released_network_type='lan'):
"""
Release the network with name 'released_network_name' from the given supernet network_id
"""
method = 'get'
response = None
if network_id is None or released_network_name is None:
self.module.exit_json(
msg="You must specify those options 'network_id', 'reserved_network_name' and 'reserved_network_size'")
matched_network_id = ""
resource_url = "networks/" + str(network_id) + "/children"
response = self._get_api_call_ansible_handler(method, resource_url)
if not response:
self.module.exit_json(
msg=" there is an error in releasing network %r from network %s." %
(network_id, released_network_name))
if response:
response = json.loads(response)
for child_net in response:
if child_net['network'] and child_net['network']['network_name'] == released_network_name:
matched_network_id = child_net['network']['network_id']
break
response = None
if matched_network_id:
method = 'delete'
resource_url = "networks/" + str(matched_network_id)
response = self._get_api_call_ansible_handler(
method, resource_url, stat_codes=[204])
else:
self.module.exit_json(
msg=" When release network , could not find the network %r from the given superent %r' " %
(released_network_name, network_id))
return response
# ---------------------------------------------------------------------------
# add_network()
# ---------------------------------------------------------------------------
def add_network(
self, network_name="", network_address="",
network_size="", network_family='4',
network_type='lan', network_location=-1):
"""
add a new LAN network into a given supernet Fusionlayer Infinity via rest api or default supernet
required fields=['network_name', 'network_family', 'network_type', 'network_address','network_size' ]
"""
method = 'post'
resource_url = 'networks'
response = None
if network_name is None or network_address is None or network_size is None:
self.module.exit_json(
msg="You must specify those options 'network_name', 'network_address' and 'network_size'")
if not network_family:
network_family = '4'
if not network_type:
network_type = 'lan'
if not network_location:
network_location = -1
payload_data = {
"network_name": network_name,
'network_address': network_address,
'network_size': network_size,
'network_family': network_family,
'network_type': network_type,
'network_location': network_location}
response = self._get_api_call_ansible_handler(
method='post', resource_url=resource_url,
stat_codes=[200], payload_data=payload_data)
return response
def main():
module = AnsibleModule(
argument_spec=dict(
server_ip=dict(type='str', required=True),
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
network_id=dict(type='str'),
ip_address=dict(type='str'),
network_name=dict(type='str'),
network_location=dict(type='int', default=-1),
network_family=dict(type='str', default='4', choices=['4', '6', 'dual']),
network_type=dict(type='str', default='lan', choices=['lan', 'shared_lan', 'supernet']),
network_address=dict(type='str'),
network_size=dict(type='str'),
action=dict(type='str', required=True, choices=[
'add_network',
'delete_network',
'get_network',
'get_network_id',
'release_ip',
'release_network',
'reserve_network',
'reserve_next_available_ip',
],),
),
required_together=(
['username', 'password'],
),
)
server_ip = module.params["server_ip"]
username = module.params["username"]
password = module.params["password"]
action = module.params["action"]
network_id = module.params["network_id"]
released_ip = module.params["ip_address"]
network_name = module.params["network_name"]
network_family = module.params["network_family"]
network_type = module.params["network_type"]
network_address = module.params["network_address"]
network_size = module.params["network_size"]
network_location = module.params["network_location"]
my_infinity = Infinity(module, server_ip, username, password)
result = ''
if action == "reserve_next_available_ip":
if network_id:
result = my_infinity.reserve_next_available_ip(network_id)
if not result:
result = 'There is an error in calling method of reserve_next_available_ip'
module.exit_json(changed=False, meta=result)
module.exit_json(changed=True, meta=result)
elif action == "release_ip":
if network_id and released_ip:
result = my_infinity.release_ip(
network_id=network_id, ip_address=released_ip)
module.exit_json(changed=True, meta=result)
elif action == "delete_network":
result = my_infinity.delete_network(
network_id=network_id, network_name=network_name)
module.exit_json(changed=True, meta=result)
elif action == "get_network_id":
result = my_infinity.get_network_id(
network_name=network_name, network_type=network_type)
module.exit_json(changed=True, meta=result)
elif action == "get_network":
result = my_infinity.get_network(
network_id=network_id, network_name=network_name)
module.exit_json(changed=True, meta=result)
elif action == "reserve_network":
result = my_infinity.reserve_network(
network_id=network_id,
reserved_network_name=network_name,
reserved_network_size=network_size,
reserved_network_family=network_family,
reserved_network_type=network_type,
reserved_network_address=network_address)
module.exit_json(changed=True, meta=result)
elif action == "release_network":
result = my_infinity.release_network(
network_id=network_id,
released_network_name=network_name,
released_network_type=network_type)
module.exit_json(changed=True, meta=result)
elif action == "add_network":
result = my_infinity.add_network(
network_name=network_name,
network_location=network_location,
network_address=network_address,
network_size=network_size,
network_family=network_family,
network_type=network_type)
module.exit_json(changed=True, meta=result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,146 @@
#!/usr/bin/python
# (c) 2017, Arie Bregman <abregman@redhat.com>
#
# This file is a module for Ansible that interacts with Network Manager
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: ip_netns
author: "Arie Bregman (@bregman-arie)"
short_description: Manage network namespaces
requirements: [ ip ]
description:
- Create or delete network namespaces using the ip command.
options:
name:
required: false
description:
- Name of the namespace
state:
required: false
default: "present"
choices: [ present, absent ]
description:
- Whether the namespace should exist
'''
EXAMPLES = '''
# Create a namespace named mario
- name: Create a namespace named mario
namespace:
name: mario
state: present
- name: Delete a namespace named luigi
namespace:
name: luigi
state: absent
'''
RETURN = '''
# Default return values
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
class Namespace(object):
"""Interface to network namespaces. """
def __init__(self, module):
self.module = module
self.name = module.params['name']
self.state = module.params['state']
def _netns(self, command):
'''Run ip nents command'''
return self.module.run_command(['ip', 'netns'] + command)
def exists(self):
'''Check if the namespace already exists'''
rc, out, err = self.module.run_command('ip netns list')
if rc != 0:
self.module.fail_json(msg=to_text(err))
return self.name in out
def add(self):
'''Create network namespace'''
rtc, out, err = self._netns(['add', self.name])
if rtc != 0:
self.module.fail_json(msg=err)
def delete(self):
'''Delete network namespace'''
rtc, out, err = self._netns(['del', self.name])
if rtc != 0:
self.module.fail_json(msg=err)
def check(self):
'''Run check mode'''
changed = False
if self.state == 'present' and self.exists():
changed = True
elif self.state == 'absent' and self.exists():
changed = True
elif self.state == 'present' and not self.exists():
changed = True
self.module.exit_json(changed=changed)
def run(self):
'''Make the necessary changes'''
changed = False
if self.state == 'absent':
if self.exists():
self.delete()
changed = True
elif self.state == 'present':
if not self.exists():
self.add()
changed = True
self.module.exit_json(changed=changed)
def main():
"""Entry point."""
module = AnsibleModule(
argument_spec={
'name': {'default': None},
'state': {'default': 'present', 'choices': ['present', 'absent']},
},
supports_check_mode=True,
)
network_namespace = Namespace(module)
if module.check_mode:
network_namespace.check()
else:
network_namespace.run()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,110 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2015, René Moser <mail@renemoser.net>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipify_facts
short_description: Retrieve the public IP of your internet gateway
description:
- If behind NAT and need to know the public IP of your internet gateway.
author:
- René Moser (@resmo)
options:
api_url:
description:
- URL of the ipify.org API service.
- C(?format=json) will be appended per default.
type: str
default: https://api.ipify.org/
timeout:
description:
- HTTP connection timeout in seconds.
type: int
default: 10
validate_certs:
description:
- When set to C(NO), SSL certificates will not be validated.
type: bool
default: yes
notes:
- Visit https://www.ipify.org to get more information.
'''
EXAMPLES = r'''
# Gather IP facts from ipify.org
- name: Get my public IP
ipify_facts:
# Gather IP facts from your own ipify service endpoint with a custom timeout
- name: Get my public IP
ipify_facts:
api_url: http://api.example.com/ipify
timeout: 20
'''
RETURN = r'''
---
ipify_public_ip:
description: Public IP of the internet gateway.
returned: success
type: str
sample: 1.2.3.4
'''
import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_text
class IpifyFacts(object):
def __init__(self):
self.api_url = module.params.get('api_url')
self.timeout = module.params.get('timeout')
def run(self):
result = {
'ipify_public_ip': None
}
(response, info) = fetch_url(module=module, url=self.api_url + "?format=json", force=True, timeout=self.timeout)
if not response:
module.fail_json(msg="No valid or no response from url %s within %s seconds (timeout)" % (self.api_url, self.timeout))
data = json.loads(to_text(response.read()))
result['ipify_public_ip'] = data.get('ip')
return result
def main():
global module
module = AnsibleModule(
argument_spec=dict(
api_url=dict(type='str', default='https://api.ipify.org/'),
timeout=dict(type='int', default=10),
validate_certs=dict(type='bool', default=True),
),
supports_check_mode=True,
)
ipify_facts = IpifyFacts().run()
ipify_facts_result = dict(changed=False, ansible_facts=ipify_facts)
module.exit_json(**ipify_facts_result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
# Copyright: (c) 2016, Aleksei Kostiuk <unitoff@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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: ipinfoio_facts
short_description: "Retrieve IP geolocation facts of a host's IP address"
description:
- "Gather IP geolocation facts of a host's IP address using ipinfo.io API"
author: "Aleksei Kostiuk (@akostyuk)"
options:
timeout:
description:
- HTTP connection timeout in seconds
required: false
default: 10
http_agent:
description:
- Set http user agent
required: false
default: "ansible-ipinfoio-module/0.0.1"
notes:
- "Check http://ipinfo.io/ for more information"
'''
EXAMPLES = '''
# Retrieve geolocation data of a host's IP address
- name: get IP geolocation data
ipinfoio_facts:
'''
RETURN = '''
ansible_facts:
description: "Dictionary of ip geolocation facts for a host's IP address"
returned: changed
type: complex
contains:
ip:
description: "Public IP address of a host"
type: str
sample: "8.8.8.8"
hostname:
description: Domain name
type: str
sample: "google-public-dns-a.google.com"
country:
description: ISO 3166-1 alpha-2 country code
type: str
sample: "US"
region:
description: State or province name
type: str
sample: "California"
city:
description: City name
type: str
sample: "Mountain View"
loc:
description: Latitude and Longitude of the location
type: str
sample: "37.3860,-122.0838"
org:
description: "organization's name"
type: str
sample: "AS3356 Level 3 Communications, Inc."
postal:
description: Postal code
type: str
sample: "94035"
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
USER_AGENT = 'ansible-ipinfoio-module/0.0.1'
class IpinfoioFacts(object):
def __init__(self, module):
self.url = 'https://ipinfo.io/json'
self.timeout = module.params.get('timeout')
self.module = module
def get_geo_data(self):
response, info = fetch_url(self.module, self.url, force=True, # NOQA
timeout=self.timeout)
try:
info['status'] == 200
except AssertionError:
self.module.fail_json(msg='Could not get {0} page, '
'check for connectivity!'.format(self.url))
else:
try:
content = response.read()
result = self.module.from_json(content.decode('utf8'))
except ValueError:
self.module.fail_json(
msg='Failed to parse the ipinfo.io response: '
'{0} {1}'.format(self.url, content))
else:
return result
def main():
module = AnsibleModule( # NOQA
argument_spec=dict(
http_agent=dict(default=USER_AGENT),
timeout=dict(type='int', default=10),
),
supports_check_mode=True,
)
ipinfoio = IpinfoioFacts(module)
ipinfoio_result = dict(
changed=False, ansible_facts=ipinfoio.get_geo_data())
module.exit_json(**ipinfoio_result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,362 @@
#!/usr/bin/python
# Copyright: (c) 2020, Christian Wollinger <cwollinger@web.de>
# 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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ipwcli_dns
short_description: Manage DNS Records for Ericsson IPWorks via ipwcli
description:
- "Manage DNS records for the Ericsson IPWorks DNS server. The module will use the ipwcli to deploy the DNS records."
requirements:
- ipwcli (installed on Ericsson IPWorks)
notes:
- To make the DNS record changes effective, you need to run C(update dnsserver) on the ipwcli.
options:
dnsname:
description:
- Name of the record.
required: true
type: str
type:
description:
- Type of the record.
required: true
type: str
choices: [ NAPTR, SRV, A, AAAA ]
container:
description:
- Sets the container zone for the record.
required: true
type: str
address:
description:
- The IP address for the A or AAAA record.
- Required for C(type=A) or C(type=AAAA)
type: str
ttl:
description:
- Sets the TTL of the record.
type: int
default: 3600
state:
description:
- Whether the record should exist or not.
type: str
choices: [ absent, present ]
default: present
priority:
description:
- Sets the priority of the SRV record.
type: int
default: 10
weight:
description:
- Sets the weight of the SRV record.
type: int
default: 10
port:
description:
- Sets the port of the SRV record.
- Required for C(type=SRV)
type: int
target:
description:
- Sets the target of the SRV record.
- Required for C(type=SRV)
type: str
order:
description:
- Sets the order of the NAPTR record.
- Required for C(type=NAPTR)
type: int
preference:
description:
- Sets the preference of the NAPTR record.
- Required for C(type=NAPTR)
type: int
flags:
description:
- Sets one of the possible flags of NAPTR record.
- Required for C(type=NAPTR)
type: str
choices: ['S', 'A', 'U', 'P']
service:
description:
- Sets the service of the NAPTR record.
- Required for C(type=NAPTR)
type: str
replacement:
description:
- Sets the replacement of the NAPTR record.
- Required for C(type=NAPTR)
type: str
username:
description:
- Username to login on ipwcli.
type: str
required: true
password:
description:
- Password to login on ipwcli.
type: str
required: true
author:
- Christian Wollinger (@cwollinger)
'''
EXAMPLES = '''
- name: Create A record
ipwcli_dns:
dnsname: example.com
type: A
container: ZoneOne
address: 127.0.0.1
- name: Remove SRV record if exists
ipwcli_dns:
dnsname: _sip._tcp.test.example.com
type: SRV
container: ZoneOne
ttl: 100
state: absent
target: example.com
port: 5060
- name: Create NAPTR record
ipwcli_dns:
dnsname: test.example.com
type: NAPTR
preference: 10
container: ZoneOne
ttl: 100
order: 10
service: 'SIP+D2T'
replacement: '_sip._tcp.test.example.com.'
flags: S
'''
RETURN = '''
record:
description: The created record from the input params
type: str
returned: always
'''
from ansible.module_utils.basic import AnsibleModule
import os
class ResourceRecord(object):
def __init__(self, module):
self.module = module
self.dnsname = module.params['dnsname']
self.dnstype = module.params['type']
self.container = module.params['container']
self.address = module.params['address']
self.ttl = module.params['ttl']
self.state = module.params['state']
self.priority = module.params['priority']
self.weight = module.params['weight']
self.port = module.params['port']
self.target = module.params['target']
self.order = module.params['order']
self.preference = module.params['preference']
self.flags = module.params['flags']
self.service = module.params['service']
self.replacement = module.params['replacement']
self.user = module.params['username']
self.password = module.params['password']
def create_naptrrecord(self):
# create NAPTR record with the given params
if not self.preference:
self.module.fail_json(msg='missing required arguments for NAPTR record: preference')
if not self.order:
self.module.fail_json(msg='missing required arguments for NAPTR record: order')
if not self.service:
self.module.fail_json(msg='missing required arguments for NAPTR record: service')
if not self.replacement:
self.module.fail_json(msg='missing required arguments for NAPTR record: replacement')
record = ('naptrrecord %s -set ttl=%s;container=%s;order=%s;preference=%s;flags="%s";service="%s";replacement="%s"'
% (self.dnsname, self.ttl, self.container, self.order, self.preference, self.flags, self.service, self.replacement))
return record
def create_srvrecord(self):
# create SRV record with the given params
if not self.port:
self.module.fail_json(msg='missing required arguments for SRV record: port')
if not self.target:
self.module.fail_json(msg='missing required arguments for SRV record: target')
record = ('srvrecord %s -set ttl=%s;container=%s;priority=%s;weight=%s;port=%s;target=%s'
% (self.dnsname, self.ttl, self.container, self.priority, self.weight, self.port, self.target))
return record
def create_arecord(self):
# create A record with the given params
if not self.address:
self.module.fail_json(msg='missing required arguments for A record: address')
if self.dnstype == 'AAAA':
record = 'aaaarecord %s %s -set ttl=%s;container=%s' % (self.dnsname, self.address, self.ttl, self.container)
else:
record = 'arecord %s %s -set ttl=%s;container=%s' % (self.dnsname, self.address, self.ttl, self.container)
return record
def list_record(self, record):
# check if the record exists via list on ipwcli
search = 'list %s' % (record.replace(';', '&&').replace('set', 'where'))
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
rc, out, err = self.module.run_command(cmd, data=search)
if 'Invalid username or password' in out:
self.module.fail_json(msg='access denied at ipwcli login: Invalid username or password')
if (('ARecord %s' % self.dnsname in out and rc == 0) or ('SRVRecord %s' % self.dnsname in out and rc == 0) or
('NAPTRRecord %s' % self.dnsname in out and rc == 0)):
return True, rc, out, err
return False, rc, out, err
def deploy_record(self, record):
# check what happens if create fails on ipworks
stdin = 'create %s' % (record)
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
rc, out, err = self.module.run_command(cmd, data=stdin)
if 'Invalid username or password' in out:
self.module.fail_json(msg='access denied at ipwcli login: Invalid username or password')
if '1 object(s) created.' in out:
return rc, out, err
else:
self.module.fail_json(msg='record creation failed', stderr=out)
def delete_record(self, record):
# check what happens if create fails on ipworks
stdin = 'delete %s' % (record.replace(';', '&&').replace('set', 'where'))
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
rc, out, err = self.module.run_command(cmd, data=stdin)
if 'Invalid username or password' in out:
self.module.fail_json(msg='access denied at ipwcli login: Invalid username or password')
if '1 object(s) were updated.' in out:
return rc, out, err
else:
self.module.fail_json(msg='record deletion failed', stderr=out)
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
dnsname=dict(type='str', required=True),
type=dict(type='str', required=True, choices=['A', 'AAAA', 'SRV', 'NAPTR']),
container=dict(type='str', required=True),
address=dict(type='str', required=False),
ttl=dict(type='int', required=False, default=3600),
state=dict(type='str', default='present', choices=['absent', 'present']),
priority=dict(type='int', required=False, default=10),
weight=dict(type='int', required=False, default=10),
port=dict(type='int', required=False),
target=dict(type='str', required=False),
order=dict(type='int', required=False),
preference=dict(type='int', required=False),
flags=dict(type='str', required=False, choices=['S', 'A', 'U', 'P']),
service=dict(type='str', required=False),
replacement=dict(type='str', required=False),
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True)
)
# define result
result = dict(
changed=False,
stdout='',
stderr='',
rc=0,
record=''
)
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
user = ResourceRecord(module)
if user.dnstype == 'NAPTR':
record = user.create_naptrrecord()
elif user.dnstype == 'SRV':
record = user.create_srvrecord()
elif user.dnstype == 'A' or user.dnstype == 'AAAA':
record = user.create_arecord()
found, rc, out, err = user.list_record(record)
if found and user.state == 'absent':
if module.check_mode:
module.exit_json(changed=True)
rc, out, err = user.delete_record(record)
result['changed'] = True
result['record'] = record
result['rc'] = rc
result['stdout'] = out
result['stderr'] = err
elif not found and user.state == 'present':
if module.check_mode:
module.exit_json(changed=True)
rc, out, err = user.deploy_record(record)
result['changed'] = True
result['record'] = record
result['rc'] = rc
result['stdout'] = out
result['stderr'] = err
else:
result['changed'] = False
result['record'] = record
result['rc'] = rc
result['stdout'] = out
result['stderr'] = err
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,292 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['deprecated'],
'supported_by': 'community'
}
DOCUMENTATION = r'''
---
module: ldap_attr
short_description: Add or remove LDAP attribute values
description:
- Add or remove LDAP attribute values.
notes:
- This only deals with attributes on existing entries. To add or remove
whole entries, see M(ldap_entry).
- The default authentication settings will attempt to use a SASL EXTERNAL
bind over a UNIX domain socket. This works well with the default Ubuntu
install for example, which includes a cn=peercred,cn=external,cn=auth ACL
rule allowing root to modify the server configuration. If you need to use
a simple bind to access your server, pass the credentials in I(bind_dn)
and I(bind_pw).
- For I(state=present) and I(state=absent), all value comparisons are
performed on the server for maximum accuracy. For I(state=exact), values
have to be compared in Python, which obviously ignores LDAP matching
rules. This should work out in most cases, but it is theoretically
possible to see spurious changes when target and actual values are
semantically identical but lexically distinct.
deprecated:
removed_in: '2.14'
why: 'The current "ldap_attr" module does not support LDAP attribute insertions or deletions with objectClass dependencies.'
alternative: 'Use M(ldap_attrs) instead. Deprecated in 2.10.'
author:
- Jiri Tyr (@jtyr)
requirements:
- python-ldap
options:
name:
description:
- The name of the attribute to modify.
type: str
required: true
state:
description:
- The state of the attribute values.
- If C(present), all given values will be added if they're missing.
- If C(absent), all given values will be removed if present.
- If C(exact), the set of values will be forced to exactly those provided and no others.
- If I(state=exact) and I(value) is an empty list, all values for this attribute will be removed.
type: str
choices: [ absent, exact, present ]
default: present
values:
description:
- The value(s) to add or remove. This can be a string or a list of
strings. The complex argument format is required in order to pass
a list of strings (see examples).
type: raw
required: true
params:
description:
- Additional module parameters.
type: dict
extends_documentation_fragment:
- community.general.ldap.documentation
'''
EXAMPLES = r'''
- name: Configure directory number 1 for example.com
ldap_attr:
dn: olcDatabase={1}hdb,cn=config
name: olcSuffix
values: dc=example,dc=com
state: exact
# The complex argument format is required here to pass a list of ACL strings.
- name: Set up the ACL
ldap_attr:
dn: olcDatabase={1}hdb,cn=config
name: olcAccess
values:
- >-
{0}to attrs=userPassword,shadowLastChange
by self write
by anonymous auth
by dn="cn=admin,dc=example,dc=com" write
by * none'
- >-
{1}to dn.base="dc=example,dc=com"
by dn="cn=admin,dc=example,dc=com" write
by * read
state: exact
- name: Declare some indexes
ldap_attr:
dn: olcDatabase={1}hdb,cn=config
name: olcDbIndex
values: "{{ item }}"
with_items:
- objectClass eq
- uid eq
- name: Set up a root user, which we can use later to bootstrap the directory
ldap_attr:
dn: olcDatabase={1}hdb,cn=config
name: "{{ item.key }}"
values: "{{ item.value }}"
state: exact
with_dict:
olcRootDN: cn=root,dc=example,dc=com
olcRootPW: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
- name: Get rid of an unneeded attribute
ldap_attr:
dn: uid=jdoe,ou=people,dc=example,dc=com
name: shadowExpire
values: []
state: exact
server_uri: ldap://localhost/
bind_dn: cn=admin,dc=example,dc=com
bind_pw: password
#
# The same as in the previous example but with the authentication details
# stored in the ldap_auth variable:
#
# ldap_auth:
# server_uri: ldap://localhost/
# bind_dn: cn=admin,dc=example,dc=com
# bind_pw: password
- name: Get rid of an unneeded attribute
ldap_attr:
dn: uid=jdoe,ou=people,dc=example,dc=com
name: shadowExpire
values: []
state: exact
params: "{{ ldap_auth }}"
'''
RETURN = r'''
modlist:
description: list of modified parameters
returned: success
type: list
sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]'
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native, to_bytes
from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
LDAP_IMP_ERR = None
try:
import ldap
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
class LdapAttr(LdapGeneric):
def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts
self.name = self.module.params['name']
self.state = self.module.params['state']
# Normalize values
if isinstance(self.module.params['values'], list):
self.values = list(map(to_bytes, self.module.params['values']))
else:
self.values = [to_bytes(self.module.params['values'])]
def add(self):
values_to_add = list(filter(self._is_value_absent, self.values))
if len(values_to_add) > 0:
modlist = [(ldap.MOD_ADD, self.name, values_to_add)]
else:
modlist = []
return modlist
def delete(self):
values_to_delete = list(filter(self._is_value_present, self.values))
if len(values_to_delete) > 0:
modlist = [(ldap.MOD_DELETE, self.name, values_to_delete)]
else:
modlist = []
return modlist
def exact(self):
try:
results = self.connection.search_s(
self.dn, ldap.SCOPE_BASE, attrlist=[self.name])
except ldap.LDAPError as e:
self.fail("Cannot search for attribute %s" % self.name, e)
current = results[0][1].get(self.name, [])
modlist = []
if frozenset(self.values) != frozenset(current):
if len(current) == 0:
modlist = [(ldap.MOD_ADD, self.name, self.values)]
elif len(self.values) == 0:
modlist = [(ldap.MOD_DELETE, self.name, None)]
else:
modlist = [(ldap.MOD_REPLACE, self.name, self.values)]
return modlist
def _is_value_present(self, value):
""" True if the target attribute has the given value. """
try:
is_present = bool(
self.connection.compare_s(self.dn, self.name, value))
except ldap.NO_SUCH_ATTRIBUTE:
is_present = False
return is_present
def _is_value_absent(self, value):
""" True if the target attribute doesn't have the given value. """
return not self._is_value_present(value)
def main():
module = AnsibleModule(
argument_spec=gen_specs(
name=dict(type='str', required=True),
params=dict(type='dict'),
state=dict(type='str', default='present', choices=['absent', 'exact', 'present']),
values=dict(type='raw', required=True),
),
supports_check_mode=True,
)
if not HAS_LDAP:
module.fail_json(msg=missing_required_lib('python-ldap'),
exception=LDAP_IMP_ERR)
# Update module parameters with user's parameters if defined
if 'params' in module.params and isinstance(module.params['params'], dict):
module.params.update(module.params['params'])
# Remove the params
module.params.pop('params', None)
# Instantiate the LdapAttr object
ldap = LdapAttr(module)
state = module.params['state']
# Perform action
if state == 'present':
modlist = ldap.add()
elif state == 'absent':
modlist = ldap.delete()
elif state == 'exact':
modlist = ldap.exact()
changed = False
if len(modlist) > 0:
changed = True
if not module.check_mode:
try:
ldap.connection.modify_s(ldap.dn, modlist)
except Exception as e:
module.fail_json(msg="Attribute action failed.", details=to_native(e))
module.exit_json(changed=changed, modlist=modlist)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,324 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Maciej Delmanowski <drybjed@gmail.com>
# Copyright: (c) 2017, Alexander Korinek <noles@a3k.net>
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = r'''
---
module: ldap_attrs
short_description: Add or remove multiple LDAP attribute values
description:
- Add or remove multiple LDAP attribute values.
notes:
- This only deals with attributes on existing entries. To add or remove
whole entries, see M(ldap_entry).
- The default authentication settings will attempt to use a SASL EXTERNAL
bind over a UNIX domain socket. This works well with the default Ubuntu
install for example, which includes a cn=peercred,cn=external,cn=auth ACL
rule allowing root to modify the server configuration. If you need to use
a simple bind to access your server, pass the credentials in I(bind_dn)
and I(bind_pw).
- For I(state=present) and I(state=absent), all value comparisons are
performed on the server for maximum accuracy. For I(state=exact), values
have to be compared in Python, which obviously ignores LDAP matching
rules. This should work out in most cases, but it is theoretically
possible to see spurious changes when target and actual values are
semantically identical but lexically distinct.
author:
- Jiri Tyr (@jtyr)
- Alexander Korinek (@noles)
- Maciej Delmanowski (@drybjed)
requirements:
- python-ldap
options:
state:
required: false
type: str
choices: [present, absent, exact]
default: present
description:
- The state of the attribute values. If C(present), all given attribute
values will be added if they're missing. If C(absent), all given
attribute values will be removed if present. If C(exact), the set of
attribute values will be forced to exactly those provided and no others.
If I(state=exact) and the attribute I(value) is empty, all values for
this attribute will be removed.
attributes:
required: true
type: dict
description:
- The attribute(s) and value(s) to add or remove. The complex argument format is required in order to pass
a list of strings (see examples).
ordered:
required: false
type: bool
default: 'no'
description:
- If C(yes), prepend list values with X-ORDERED index numbers in all
attributes specified in the current task. This is useful mostly with
I(olcAccess) attribute to easily manage LDAP Access Control Lists.
extends_documentation_fragment:
- community.general.ldap.documentation
'''
EXAMPLES = r'''
- name: Configure directory number 1 for example.com
ldap_attrs:
dn: olcDatabase={1}hdb,cn=config
attributes:
olcSuffix: dc=example,dc=com
state: exact
# The complex argument format is required here to pass a list of ACL strings.
- name: Set up the ACL
ldap_attrs:
dn: olcDatabase={1}hdb,cn=config
attributes:
olcAccess:
- >-
{0}to attrs=userPassword,shadowLastChange
by self write
by anonymous auth
by dn="cn=admin,dc=example,dc=com" write
by * none'
- >-
{1}to dn.base="dc=example,dc=com"
by dn="cn=admin,dc=example,dc=com" write
by * read
state: exact
# An alternative approach with automatic X-ORDERED numbering
- name: Set up the ACL
ldap_attrs:
dn: olcDatabase={1}hdb,cn=config
attributes:
olcAccess:
- >-
to attrs=userPassword,shadowLastChange
by self write
by anonymous auth
by dn="cn=admin,dc=example,dc=com" write
by * none'
- >-
to dn.base="dc=example,dc=com"
by dn="cn=admin,dc=example,dc=com" write
by * read
ordered: yes
state: exact
- name: Declare some indexes
ldap_attrs:
dn: olcDatabase={1}hdb,cn=config
attributes:
olcDbIndex:
- objectClass eq
- uid eq
- name: Set up a root user, which we can use later to bootstrap the directory
ldap_attrs:
dn: olcDatabase={1}hdb,cn=config
attributes:
olcRootDN: cn=root,dc=example,dc=com
olcRootPW: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
state: exact
- name: Remove an attribute with a specific value
ldap_attrs:
dn: uid=jdoe,ou=people,dc=example,dc=com
attributes:
description: "An example user account"
state: absent
server_uri: ldap://localhost/
bind_dn: cn=admin,dc=example,dc=com
bind_pw: password
- name: Remove specified attribute(s) from an entry
ldap_attrs:
dn: uid=jdoe,ou=people,dc=example,dc=com
attributes:
description: []
state: exact
server_uri: ldap://localhost/
bind_dn: cn=admin,dc=example,dc=com
bind_pw: password
'''
RETURN = r'''
modlist:
description: list of modified parameters
returned: success
type: list
sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]'
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native, to_bytes
from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
import re
LDAP_IMP_ERR = None
try:
import ldap
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
class LdapAttrs(LdapGeneric):
def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts
self.attrs = self.module.params['attributes']
self.state = self.module.params['state']
self.ordered = self.module.params['ordered']
def _order_values(self, values):
""" Preprend X-ORDERED index numbers to attribute's values. """
ordered_values = []
if isinstance(values, list):
for index, value in enumerate(values):
cleaned_value = re.sub(r'^\{\d+\}', '', value)
ordered_values.append('{' + str(index) + '}' + cleaned_value)
return ordered_values
def _normalize_values(self, values):
""" Normalize attribute's values. """
norm_values = []
if isinstance(values, list):
if self.ordered:
norm_values = list(map(to_bytes,
self._order_values(list(map(str,
values)))))
else:
norm_values = list(map(to_bytes, values))
else:
norm_values = [to_bytes(str(values))]
return norm_values
def add(self):
modlist = []
for name, values in self.module.params['attributes'].items():
norm_values = self._normalize_values(values)
for value in norm_values:
if self._is_value_absent(name, value):
modlist.append((ldap.MOD_ADD, name, value))
return modlist
def delete(self):
modlist = []
for name, values in self.module.params['attributes'].items():
norm_values = self._normalize_values(values)
for value in norm_values:
if self._is_value_present(name, value):
modlist.append((ldap.MOD_DELETE, name, value))
return modlist
def exact(self):
modlist = []
for name, values in self.module.params['attributes'].items():
norm_values = self._normalize_values(values)
try:
results = self.connection.search_s(
self.dn, ldap.SCOPE_BASE, attrlist=[name])
except ldap.LDAPError as e:
self.fail("Cannot search for attribute %s" % name, e)
current = results[0][1].get(name, [])
if frozenset(norm_values) != frozenset(current):
if len(current) == 0:
modlist.append((ldap.MOD_ADD, name, norm_values))
elif len(norm_values) == 0:
modlist.append((ldap.MOD_DELETE, name, None))
else:
modlist.append((ldap.MOD_REPLACE, name, norm_values))
return modlist
def _is_value_present(self, name, value):
""" True if the target attribute has the given value. """
try:
is_present = bool(
self.connection.compare_s(self.dn, name, value))
except ldap.NO_SUCH_ATTRIBUTE:
is_present = False
return is_present
def _is_value_absent(self, name, value):
""" True if the target attribute doesn't have the given value. """
return not self._is_value_present(name, value)
def main():
module = AnsibleModule(
argument_spec=gen_specs(
attributes=dict(type='dict', required=True),
ordered=dict(type='bool', default=False, required=False),
state=dict(type='str', default='present', choices=['absent', 'exact', 'present']),
),
supports_check_mode=True,
)
if not HAS_LDAP:
module.fail_json(msg=missing_required_lib('python-ldap'),
exception=LDAP_IMP_ERR)
# Instantiate the LdapAttr object
ldap = LdapAttrs(module)
state = module.params['state']
# Perform action
if state == 'present':
modlist = ldap.add()
elif state == 'absent':
modlist = ldap.delete()
elif state == 'exact':
modlist = ldap.exact()
changed = False
if len(modlist) > 0:
changed = True
if not module.check_mode:
try:
ldap.connection.modify_s(ldap.dn, modlist)
except Exception as e:
module.fail_json(msg="Attribute action failed.", details=to_native(e))
module.exit_json(changed=changed, modlist=modlist)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,252 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ldap_entry
short_description: Add or remove LDAP entries.
description:
- Add or remove LDAP entries. This module only asserts the existence or
non-existence of an LDAP entry, not its attributes. To assert the
attribute values of an entry, see M(ldap_attr).
notes:
- The default authentication settings will attempt to use a SASL EXTERNAL
bind over a UNIX domain socket. This works well with the default Ubuntu
install for example, which includes a cn=peercred,cn=external,cn=auth ACL
rule allowing root to modify the server configuration. If you need to use
a simple bind to access your server, pass the credentials in I(bind_dn)
and I(bind_pw).
author:
- Jiri Tyr (@jtyr)
requirements:
- python-ldap
options:
attributes:
description:
- If I(state=present), attributes necessary to create an entry. Existing
entries are never modified. To assert specific attribute values on an
existing entry, use M(ldap_attr) module instead.
objectClass:
description:
- If I(state=present), value or list of values to use when creating
the entry. It can either be a string or an actual list of
strings.
params:
description:
- List of options which allows to overwrite any of the task or the
I(attributes) options. To remove an option, set the value of the option
to C(null).
state:
description:
- The target state of the entry.
choices: [present, absent]
default: present
extends_documentation_fragment:
- community.general.ldap.documentation
'''
EXAMPLES = """
- name: Make sure we have a parent entry for users
ldap_entry:
dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
- name: Make sure we have an admin user
ldap_entry:
dn: cn=admin,dc=example,dc=com
objectClass:
- simpleSecurityObject
- organizationalRole
attributes:
description: An LDAP administrator
userPassword: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
- name: Get rid of an old entry
ldap_entry:
dn: ou=stuff,dc=example,dc=com
state: absent
server_uri: ldap://localhost/
bind_dn: cn=admin,dc=example,dc=com
bind_pw: password
#
# The same as in the previous example but with the authentication details
# stored in the ldap_auth variable:
#
# ldap_auth:
# server_uri: ldap://localhost/
# bind_dn: cn=admin,dc=example,dc=com
# bind_pw: password
- name: Get rid of an old entry
ldap_entry:
dn: ou=stuff,dc=example,dc=com
state: absent
params: "{{ ldap_auth }}"
"""
RETURN = """
# Default return values
"""
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_native, to_bytes
from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
LDAP_IMP_ERR = None
try:
import ldap.modlist
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
class LdapEntry(LdapGeneric):
def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts
self.state = self.module.params['state']
# Add the objectClass into the list of attributes
self.module.params['attributes']['objectClass'] = (
self.module.params['objectClass'])
# Load attributes
if self.state == 'present':
self.attrs = self._load_attrs()
def _load_attrs(self):
""" Turn attribute's value to array. """
attrs = {}
for name, value in self.module.params['attributes'].items():
if name not in attrs:
attrs[name] = []
if isinstance(value, list):
attrs[name] = list(map(to_bytes, value))
else:
attrs[name].append(to_bytes(value))
return attrs
def add(self):
""" If self.dn does not exist, returns a callable that will add it. """
def _add():
self.connection.add_s(self.dn, modlist)
if not self._is_entry_present():
modlist = ldap.modlist.addModlist(self.attrs)
action = _add
else:
action = None
return action
def delete(self):
""" If self.dn exists, returns a callable that will delete it. """
def _delete():
self.connection.delete_s(self.dn)
if self._is_entry_present():
action = _delete
else:
action = None
return action
def _is_entry_present(self):
try:
self.connection.search_s(self.dn, ldap.SCOPE_BASE)
except ldap.NO_SUCH_OBJECT:
is_present = False
else:
is_present = True
return is_present
def main():
module = AnsibleModule(
argument_spec=gen_specs(
attributes=dict(default={}, type='dict'),
objectClass=dict(type='raw'),
params=dict(type='dict'),
state=dict(default='present', choices=['present', 'absent']),
),
supports_check_mode=True,
)
if not HAS_LDAP:
module.fail_json(msg=missing_required_lib('python-ldap'),
exception=LDAP_IMP_ERR)
state = module.params['state']
# Check if objectClass is present when needed
if state == 'present' and module.params['objectClass'] is None:
module.fail_json(msg="At least one objectClass must be provided.")
# Check if objectClass is of the correct type
if (
module.params['objectClass'] is not None and not (
isinstance(module.params['objectClass'], string_types) or
isinstance(module.params['objectClass'], list))):
module.fail_json(msg="objectClass must be either a string or a list.")
# Update module parameters with user's parameters if defined
if 'params' in module.params and isinstance(module.params['params'], dict):
for key, val in module.params['params'].items():
if key in module.argument_spec:
module.params[key] = val
else:
module.params['attributes'][key] = val
# Remove the params
module.params.pop('params', None)
# Instantiate the LdapEntry object
ldap = LdapEntry(module)
# Get the action function
if state == 'present':
action = ldap.add()
elif state == 'absent':
action = ldap.delete()
# Perform the action
if action is not None and not module.check_mode:
try:
action()
except Exception as e:
module.fail_json(msg="Entry action failed.", details=to_native(e), exception=traceback.format_exc())
module.exit_json(changed=(action is not None))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,150 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017-2018, Keller Fuchs <kellerfuchs@hashbang.sh>
# 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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ldap_passwd
short_description: Set passwords in LDAP.
description:
- Set a password for an LDAP entry. This module only asserts that
a given password is valid for a given entry. To assert the
existence of an entry, see M(ldap_entry).
notes:
- The default authentication settings will attempt to use a SASL EXTERNAL
bind over a UNIX domain socket. This works well with the default Ubuntu
install for example, which includes a cn=peercred,cn=external,cn=auth ACL
rule allowing root to modify the server configuration. If you need to use
a simple bind to access your server, pass the credentials in I(bind_dn)
and I(bind_pw).
author:
- Keller Fuchs (@KellerFuchs)
requirements:
- python-ldap
options:
passwd:
required: true
description:
- The (plaintext) password to be set for I(dn).
extends_documentation_fragment:
- community.general.ldap.documentation
'''
EXAMPLES = """
- name: Set a password for the admin user
ldap_passwd:
dn: cn=admin,dc=example,dc=com
passwd: "{{ vault_secret }}"
- name: Setting passwords in bulk
ldap_passwd:
dn: "{{ item.key }}"
passwd: "{{ item.value }}"
with_dict:
alice: alice123123
bob: "|30b!"
admin: "{{ vault_secret }}"
"""
RETURN = """
modlist:
description: list of modified parameters
returned: success
type: list
sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]'
"""
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
LDAP_IMP_ERR = None
try:
import ldap
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
class LdapPasswd(LdapGeneric):
def __init__(self, module):
LdapGeneric.__init__(self, module)
# Shortcuts
self.passwd = self.module.params['passwd']
def passwd_check(self):
try:
tmp_con = ldap.initialize(self.server_uri)
except ldap.LDAPError as e:
self.fail("Cannot initialize LDAP connection", e)
if self.start_tls:
try:
tmp_con.start_tls_s()
except ldap.LDAPError as e:
self.fail("Cannot start TLS.", e)
try:
tmp_con.simple_bind_s(self.dn, self.passwd)
except ldap.INVALID_CREDENTIALS:
return True
except ldap.LDAPError as e:
self.fail("Cannot bind to the server.", e)
else:
return False
finally:
tmp_con.unbind()
def passwd_set(self):
# Exit early if the password is already valid
if not self.passwd_check():
return False
# Change the password (or throw an exception)
try:
self.connection.passwd_s(self.dn, None, self.passwd)
except ldap.LDAPError as e:
self.fail("Unable to set password", e)
# Password successfully changed
return True
def main():
module = AnsibleModule(
argument_spec=gen_specs(passwd=dict(no_log=True)),
supports_check_mode=True,
)
if not HAS_LDAP:
module.fail_json(msg=missing_required_lib('python-ldap'),
exception=LDAP_IMP_ERR)
ldap = LdapPasswd(module)
if module.check_mode:
module.exit_json(changed=ldap.passwd_check())
module.exit_json(changed=ldap.passwd_set())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,84 @@
#!/usr/bin/python
#
# Copyright: Ansible Project
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: lldp
requirements: [ lldpctl ]
short_description: get details reported by lldp
description:
- Reads data out of lldpctl
options: {}
author: "Andy Hill (@andyhky)"
notes:
- Requires lldpd running and lldp enabled on switches
'''
EXAMPLES = '''
# Retrieve switch/port information
- name: Gather information from lldp
lldp:
- name: Print each switch/port
debug:
msg: "{{ lldp[item]['chassis']['name'] }} / {{ lldp[item]['port']['ifname'] }}"
with_items: "{{ lldp.keys() }}"
# TASK: [Print each switch/port] ***********************************************************
# ok: [10.13.0.22] => (item=eth2) => {"item": "eth2", "msg": "switch1.example.com / Gi0/24"}
# ok: [10.13.0.22] => (item=eth1) => {"item": "eth1", "msg": "switch2.example.com / Gi0/3"}
# ok: [10.13.0.22] => (item=eth0) => {"item": "eth0", "msg": "switch3.example.com / Gi0/3"}
'''
from ansible.module_utils.basic import AnsibleModule
def gather_lldp(module):
cmd = ['lldpctl', '-f', 'keyvalue']
rc, output, err = module.run_command(cmd)
if output:
output_dict = {}
current_dict = {}
lldp_entries = output.split("\n")
for entry in lldp_entries:
if entry.startswith('lldp'):
path, value = entry.strip().split("=", 1)
path = path.split(".")
path_components, final = path[:-1], path[-1]
else:
value = current_dict[final] + '\n' + entry
current_dict = output_dict
for path_component in path_components:
current_dict[path_component] = current_dict.get(path_component, {})
current_dict = current_dict[path_component]
current_dict[final] = value
return output_dict
def main():
module = AnsibleModule({})
lldp_output = gather_lldp(module)
try:
data = {'lldp': lldp_output['lldp']}
module.exit_json(ansible_facts=data)
except TypeError:
module.fail_json(msg="lldpctl command failed. is lldpd running?")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,265 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018 Nicolai Buchwitz <nb@tipi-net.de>
# 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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: netcup_dns
notes: []
short_description: manage Netcup DNS records
description:
- "Manages DNS records via the Netcup API, see the docs U(https://ccp.netcup.net/run/webservice/servers/endpoint.php)"
options:
api_key:
description:
- API key for authentication, must be obtained via the netcup CCP (U(https://ccp.netcup.net))
required: True
api_password:
description:
- API password for authentication, must be obtained via the netcup CCP (https://ccp.netcup.net)
required: True
customer_id:
description:
- Netcup customer id
required: True
domain:
description:
- Domainname the records should be added / removed
required: True
record:
description:
- Record to add or delete, supports wildcard (*). Default is C(@) (e.g. the zone name)
default: "@"
aliases: [ name ]
type:
description:
- Record type
choices: ['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', 'TLSA', 'NS', 'DS']
required: True
value:
description:
- Record value
required: true
solo:
type: bool
default: False
description:
- Whether the record should be the only one for that record type and record name. Only use with C(state=present)
- This will delete all other records with the same record name and type.
priority:
description:
- Record priority. Required for C(type=MX)
required: False
state:
description:
- Whether the record should exist or not
required: False
default: present
choices: [ 'present', 'absent' ]
requirements:
- "nc-dnsapi >= 0.1.3"
author: "Nicolai Buchwitz (@nbuchwitz)"
'''
EXAMPLES = '''
- name: Create a record of type A
netcup_dns:
api_key: "..."
api_password: "..."
customer_id: "..."
domain: "example.com"
name: "mail"
type: "A"
value: "127.0.0.1"
- name: Delete that record
netcup_dns:
api_key: "..."
api_password: "..."
customer_id: "..."
domain: "example.com"
name: "mail"
type: "A"
value: "127.0.0.1"
state: absent
- name: Create a wildcard record
netcup_dns:
api_key: "..."
api_password: "..."
customer_id: "..."
domain: "example.com"
name: "*"
type: "A"
value: "127.0.1.1"
- name: Set the MX record for example.com
netcup_dns:
api_key: "..."
api_password: "..."
customer_id: "..."
domain: "example.com"
type: "MX"
value: "mail.example.com"
- name: Set a record and ensure that this is the only one
netcup_dns:
api_key: "..."
api_password: "..."
customer_id: "..."
name: "demo"
domain: "example.com"
type: "AAAA"
value: "::1"
solo: true
'''
RETURN = '''
records:
description: list containing all records
returned: success
type: complex
contains:
name:
description: the record name
returned: success
type: str
sample: fancy-hostname
type:
description: the record type
returned: succcess
type: str
sample: A
value:
description: the record destination
returned: success
type: str
sample: 127.0.0.1
priority:
description: the record priority (only relevant if type=MX)
returned: success
type: int
sample: 0
id:
description: internal id of the record
returned: success
type: int
sample: 12345
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
NCDNSAPI_IMP_ERR = None
try:
import nc_dnsapi
from nc_dnsapi import DNSRecord
HAS_NCDNSAPI = True
except ImportError:
NCDNSAPI_IMP_ERR = traceback.format_exc()
HAS_NCDNSAPI = False
def main():
module = AnsibleModule(
argument_spec=dict(
api_key=dict(required=True, no_log=True),
api_password=dict(required=True, no_log=True),
customer_id=dict(required=True, type='int'),
domain=dict(required=True),
record=dict(required=False, default='@', aliases=['name']),
type=dict(required=True, choices=['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', 'TLSA', 'NS', 'DS']),
value=dict(required=True),
priority=dict(required=False, type='int'),
solo=dict(required=False, type='bool', default=False),
state=dict(required=False, choices=['present', 'absent'], default='present'),
),
supports_check_mode=True
)
if not HAS_NCDNSAPI:
module.fail_json(msg=missing_required_lib('nc-dnsapi'), exception=NCDNSAPI_IMP_ERR)
api_key = module.params.get('api_key')
api_password = module.params.get('api_password')
customer_id = module.params.get('customer_id')
domain = module.params.get('domain')
record_type = module.params.get('type')
record = module.params.get('record')
value = module.params.get('value')
priority = module.params.get('priority')
solo = module.params.get('solo')
state = module.params.get('state')
if record_type == 'MX' and not priority:
module.fail_json(msg="record type MX required the 'priority' argument")
has_changed = False
all_records = []
try:
with nc_dnsapi.Client(customer_id, api_key, api_password) as api:
all_records = api.dns_records(domain)
record = DNSRecord(record, record_type, value, priority=priority)
# try to get existing record
record_exists = False
for r in all_records:
if r == record:
record_exists = True
record = r
break
if state == 'present':
if solo:
obsolete_records = [r for r in all_records if
r.hostname == record.hostname
and r.type == record.type
and not r.destination == record.destination]
if obsolete_records:
if not module.check_mode:
all_records = api.delete_dns_records(domain, obsolete_records)
has_changed = True
if not record_exists:
if not module.check_mode:
all_records = api.add_dns_record(domain, record)
has_changed = True
elif state == 'absent' and record_exists:
if not module.check_mode:
all_records = api.delete_dns_record(domain, record)
has_changed = True
except Exception as ex:
module.fail_json(msg=ex.message)
module.exit_json(changed=has_changed, result={"records": [record_data(r) for r in all_records]})
def record_data(r):
return {"name": r.hostname, "type": r.type, "value": r.destination, "priority": r.priority, "id": r.id}
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,174 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_a_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS A records
description:
- Adds and/or removes instances of A record objects from
Infoblox NIOS servers. This module manages NIOS C(record:a) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this A record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
ipv4addr:
description:
- Configures the IPv4 address for this A record. Users can dynamically
allocate ipv4 address to A record by passing dictionary containing,
I(nios_next_ip) and I(CIDR network range). See example
required: true
aliases:
- ipv4
ttl:
description:
- Configures the TTL to be associated with this A record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure an A record
nios_a_record:
name: a.ansible.com
ipv4: 192.168.10.1
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing A record
nios_a_record:
name: a.ansible.com
ipv4: 192.168.10.1
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove an A record from the system
nios_a_record:
name: a.ansible.com
ipv4: 192.168.10.1
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update an A record name
nios_a_record:
name: {new_name: a_new.ansible.com, old_name: a.ansible.com}
ipv4: 192.168.10.1
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: dynamically add a record to next available ip
nios_a_record:
name: a.ansible.com
ipv4: {nios_next_ip: 192.168.10.0/24}
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_A_RECORD
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
ipv4addr=dict(aliases=['ipv4'], ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_A_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,161 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_aaaa_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS AAAA records
description:
- Adds and/or removes instances of AAAA record objects from
Infoblox NIOS servers. This module manages NIOS C(record:aaaa) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this AAAA record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
ipv6addr:
description:
- Configures the IPv6 address for this AAAA record.
required: true
aliases:
- ipv6
ttl:
description:
- Configures the TTL to be associated with this AAAA record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure an AAAA record
nios_aaaa_record:
name: aaaa.ansible.com
ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing AAAA record
nios_aaaa_record:
name: aaaa.ansible.com
ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove an AAAA record from the system
nios_aaaa_record:
name: aaaa.ansible.com
ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update an AAAA record name
nios_aaaa_record:
name: {new_name: aaaa_new.ansible.com, old_name: aaaa.ansible.com}
ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_AAAA_RECORD
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
ipv6addr=dict(aliases=['ipv6'], ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_AAAA_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,150 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_cname_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS CNAME records
description:
- Adds and/or removes instances of CNAME record objects from
Infoblox NIOS servers. This module manages NIOS C(record:cname) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this CNAME record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
canonical:
description:
- Configures the canonical name for this CNAME record.
required: true
aliases:
- cname
ttl:
description:
- Configures the TTL to be associated with this CNAME record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a CNAME record
nios_cname_record:
name: cname.ansible.com
canonical: realhost.ansible.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing CNAME record
nios_cname_record:
name: cname.ansible.com
canonical: realhost.ansible.com
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a CNAME record from the system
nios_cname_record:
name: cname.ansible.com
canonical: realhost.ansible.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_CNAME_RECORD
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
canonical=dict(aliases=['cname'], ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_CNAME_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_dns_view
author: "Peter Sprygada (@privateip)"
short_description: Configure Infoblox NIOS DNS views
description:
- Adds and/or removes instances of DNS view objects from
Infoblox NIOS servers. This module manages NIOS C(view) objects
using the Infoblox WAPI interface over REST.
- Updates instances of DNS view object from Infoblox NIOS servers.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system. User can also update the hostname as it is possible
to pass a dict containing I(new_name), I(old_name). See examples.
required: true
aliases:
- view
network_view:
description:
- Specifies the name of the network view to assign the configured
DNS view to. The network view must already be configured on the
target system.
required: true
default: default
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
required: false
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
required: false
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
required: false
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a new dns view instance
nios_dns_view:
name: ansible-dns
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update the comment for dns view
nios_dns_view:
name: ansible-dns
comment: this is an example comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove the dns view instance
nios_dns_view:
name: ansible-dns
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update the dns view instance
nios_dns_view:
name: {new_name: ansible-dns-new, old_name: ansible-dns}
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_DNS_VIEW
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, aliases=['view'], ib_req=True),
network_view=dict(default='default', ib_req=True),
extattrs=dict(type='dict'),
comment=dict()
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_DNS_VIEW, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,270 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_fixed_address
author: "Sumit Jaiswal (@sjaiswal)"
short_description: Configure Infoblox NIOS DHCP Fixed Address
description:
- A fixed address is a specific IP address that a DHCP server
always assigns when a lease request comes from a particular
MAC address of the client.
- Supports both IPV4 and IPV6 internet protocols
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the hostname with which fixed DHCP ip-address is stored
for respective mac.
required: false
ipaddr:
description:
- IPV4/V6 address of the fixed address.
required: true
mac:
description:
- The MAC address of the interface.
required: true
network:
description:
- Specifies the network range in which ipaddr exists.
aliases:
- network
network_view:
description:
- Configures the name of the network view to associate with this
configured instance.
required: false
default: default
options:
description:
- Configures the set of DHCP options to be included as part of
the configured network instance. This argument accepts a list
of values (see suboptions). When configuring suboptions at
least one of C(name) or C(num) must be specified.
suboptions:
name:
description:
- The name of the DHCP option to configure
num:
description:
- The number of the DHCP option to configure
value:
description:
- The value of the DHCP option specified by C(name)
required: true
use_option:
description:
- Only applies to a subset of options (see NIOS API documentation)
type: bool
default: 'yes'
vendor_class:
description:
- The name of the space this DHCP option is associated to
default: DHCP
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure ipv4 dhcp fixed address
nios_fixed_address:
name: ipv4_fixed
ipaddr: 192.168.10.1
mac: 08:6d:41:e8:fd:e8
network: 192.168.10.0/24
network_view: default
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a ipv6 dhcp fixed address
nios_fixed_address:
name: ipv6_fixed
ipaddr: fe80::1/10
mac: 08:6d:41:e8:fd:e8
network: fe80::/64
network_view: default
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: set dhcp options for a ipv4 fixed address
nios_fixed_address:
name: ipv4_fixed
ipaddr: 192.168.10.1
mac: 08:6d:41:e8:fd:e8
network: 192.168.10.0/24
network_view: default
comment: this is a test comment
options:
- name: domain-name
value: ansible.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a ipv4 dhcp fixed address
nios_fixed_address:
name: ipv4_fixed
ipaddr: 192.168.10.1
mac: 08:6d:41:e8:fd:e8
network: 192.168.10.0/24
network_view: default
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import validate_ip_address, validate_ip_v6_address
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_IPV4_FIXED_ADDRESS, NIOS_IPV6_FIXED_ADDRESS
def options(module):
''' Transforms the module argument into a valid WAPI struct
This function will transform the options argument into a structure that
is a valid WAPI structure in the format of:
{
name: <value>,
num: <value>,
value: <value>,
use_option: <value>,
vendor_class: <value>
}
It will remove any options that are set to None since WAPI will error on
that condition. The use_option field only applies
to special options that are displayed separately from other options and
have a use flag. This function removes the use_option flag from all
other options. It will also verify that either `name` or `num` is
set in the structure but does not validate the values are equal.
The remainder of the value validation is performed by WAPI
'''
special_options = ['routers', 'router-templates', 'domain-name-servers',
'domain-name', 'broadcast-address', 'broadcast-address-offset',
'dhcp-lease-time', 'dhcp6.name-servers']
options = list()
for item in module.params['options']:
opt = dict([(k, v) for k, v in iteritems(item) if v is not None])
if 'name' not in opt and 'num' not in opt:
module.fail_json(msg='one of `name` or `num` is required for option value')
if opt['name'] not in special_options:
del opt['use_option']
options.append(opt)
return options
def validate_ip_addr_type(ip, arg_spec, module):
'''This function will check if the argument ip is type v4/v6 and return appropriate infoblox network type
'''
check_ip = ip.split('/')
if validate_ip_address(check_ip[0]) and 'ipaddr' in arg_spec:
arg_spec['ipv4addr'] = arg_spec.pop('ipaddr')
module.params['ipv4addr'] = module.params.pop('ipaddr')
return NIOS_IPV4_FIXED_ADDRESS, arg_spec, module
elif validate_ip_v6_address(check_ip[0]) and 'ipaddr' in arg_spec:
arg_spec['ipv6addr'] = arg_spec.pop('ipaddr')
module.params['ipv6addr'] = module.params.pop('ipaddr')
return NIOS_IPV6_FIXED_ADDRESS, arg_spec, module
def main():
''' Main entry point for module execution
'''
option_spec = dict(
# one of name or num is required; enforced by the function options()
name=dict(),
num=dict(type='int'),
value=dict(required=True),
use_option=dict(type='bool', default=True),
vendor_class=dict(default='DHCP')
)
ib_spec = dict(
name=dict(required=True),
ipaddr=dict(required=True, aliases=['ipaddr'], ib_req=True),
mac=dict(required=True, aliases=['mac'], ib_req=True),
network=dict(required=True, aliases=['network']),
network_view=dict(default='default', aliases=['network_view']),
options=dict(type='list', elements='dict', options=option_spec, transform=options),
extattrs=dict(type='dict'),
comment=dict()
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
# to get the argument ipaddr
obj_filter = dict([(k, module.params[k]) for k, v in iteritems(ib_spec) if v.get('ib_req')])
# to modify argument based on ipaddr type i.e. IPV4/IPV6
fixed_address_ip_type, ib_spec, module = validate_ip_addr_type(obj_filter['ipaddr'], ib_spec, module)
wapi = WapiModule(module)
result = wapi.run(fixed_address_ip_type, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,344 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_host_record
author: "Peter Sprygada (@privateip)"
short_description: Configure Infoblox NIOS host records
description:
- Adds and/or removes instances of host record objects from
Infoblox NIOS servers. This module manages NIOS C(record:host) objects
using the Infoblox WAPI interface over REST.
- Updates instances of host record object from Infoblox NIOS servers.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system. User can also update the hostname as it is possible
to pass a dict containing I(new_name), I(old_name). See examples.
required: true
view:
description:
- Sets the DNS view to associate this host record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
configure_for_dns:
description:
- Sets the DNS to particular parent. If user needs to bypass DNS
user can make the value to false.
type: bool
required: false
default: true
aliases:
- dns
ipv4addrs:
description:
- Configures the IPv4 addresses for this host record. This argument
accepts a list of values (see suboptions)
aliases:
- ipv4
suboptions:
ipv4addr:
description:
- Configures the IPv4 address for the host record. Users can dynamically
allocate ipv4 address to host record by passing dictionary containing,
I(nios_next_ip) and I(CIDR network range). If user wants to add or
remove the ipv4 address from existing record, I(add/remove)
params need to be used. See examples
required: true
aliases:
- address
configure_for_dhcp:
description:
- Configure the host_record over DHCP instead of DNS, if user
changes it to true, user need to mention MAC address to configure
required: false
aliases:
- dhcp
mac:
description:
- Configures the hardware MAC address for the host record. If user makes
DHCP to true, user need to mention MAC address.
required: false
aliases:
- mac
add:
description:
- If user wants to add the ipv4 address to an existing host record.
Note that with I(add) user will have to keep the I(state) as I(present),
as new IP address is allocated to existing host record. See examples.
type: bool
required: false
aliases:
- add
remove:
description:
- If user wants to remove the ipv4 address from an existing host record.
Note that with I(remove) user will have to change the I(state) to I(absent),
as IP address is de-allocated from an existing host record. See examples.
type: bool
required: false
aliases:
- remove
ipv6addrs:
description:
- Configures the IPv6 addresses for the host record. This argument
accepts a list of values (see options)
aliases:
- ipv6
suboptions:
ipv6addr:
description:
- Configures the IPv6 address for the host record
required: true
aliases:
- address
configure_for_dhcp:
description:
- Configure the host_record over DHCP instead of DNS, if user
changes it to true, user need to mention MAC address to configure
required: false
aliases:
- dhcp
aliases:
description:
- Configures an optional list of additional aliases to add to the host
record. These are equivalent to CNAMEs but held within a host
record. Must be in list format.
ttl:
description:
- Configures the TTL to be associated with this host record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure an ipv4 host record
nios_host_record:
name: host.ansible.com
ipv4:
- address: 192.168.10.1
aliases:
- cname.ansible.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing host record
nios_host_record:
name: host.ansible.com
ipv4:
- address: 192.168.10.1
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a host record from the system
nios_host_record:
name: host.ansible.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update an ipv4 host record
nios_host_record:
name: {new_name: host-new.ansible.com, old_name: host.ansible.com}
ipv4:
- address: 192.168.10.1
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: create an ipv4 host record bypassing DNS
nios_host_record:
name: new_host
ipv4:
- address: 192.168.10.1
dns: false
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: create an ipv4 host record over DHCP
nios_host_record:
name: host.ansible.com
ipv4:
- address: 192.168.10.1
dhcp: true
mac: 00-80-C8-E3-4C-BD
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: dynamically add host record to next available ip
nios_host_record:
name: host.ansible.com
ipv4:
- address: {nios_next_ip: 192.168.10.0/24}
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add ip to host record
nios_host_record:
name: host.ansible.com
ipv4:
- address: 192.168.10.2
add: true
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove ip to host record
nios_host_record:
name: host.ansible.com
ipv4:
- address: 192.168.10.1
remove: true
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_HOST_RECORD
def ipaddr(module, key, filtered_keys=None):
''' Transforms the input value into a struct supported by WAPI
This function will transform the input from the playbook into a struct
that is valid for WAPI in the form of:
{
ipv4addr: <value>,
mac: <value>
}
This function does not validate the values are properly formatted or in
the acceptable range, that is left to WAPI.
'''
filtered_keys = filtered_keys or list()
objects = list()
for item in module.params[key]:
objects.append(dict([(k, v) for k, v in iteritems(item) if v is not None and k not in filtered_keys]))
return objects
def ipv4addrs(module):
return ipaddr(module, 'ipv4addrs', filtered_keys=['address', 'dhcp'])
def ipv6addrs(module):
return ipaddr(module, 'ipv6addrs', filtered_keys=['address', 'dhcp'])
def main():
''' Main entry point for module execution
'''
ipv4addr_spec = dict(
ipv4addr=dict(required=True, aliases=['address'], ib_req=True),
configure_for_dhcp=dict(type='bool', required=False, aliases=['dhcp'], ib_req=True),
mac=dict(required=False, aliases=['mac'], ib_req=True),
add=dict(type='bool', aliases=['add'], required=False),
remove=dict(type='bool', aliases=['remove'], required=False)
)
ipv6addr_spec = dict(
ipv6addr=dict(required=True, aliases=['address'], ib_req=True),
configure_for_dhcp=dict(type='bool', required=False, aliases=['configure_for_dhcp'], ib_req=True),
mac=dict(required=False, aliases=['mac'], ib_req=True)
)
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
ipv4addrs=dict(type='list', aliases=['ipv4'], elements='dict', options=ipv4addr_spec, transform=ipv4addrs),
ipv6addrs=dict(type='list', aliases=['ipv6'], elements='dict', options=ipv6addr_spec, transform=ipv6addrs),
configure_for_dns=dict(type='bool', default=True, required=False, aliases=['dns'], ib_req=True),
aliases=dict(type='list'),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_HOST_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,493 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_member
author: "Krishna Vasudevan (@krisvasudevan)"
short_description: Configure Infoblox NIOS members
description:
- Adds and/or removes Infoblox NIOS servers. This module manages NIOS C(member) objects using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
host_name:
description:
- Specifies the host name of the member to either add or remove from
the NIOS instance.
required: true
aliases:
- name
vip_setting:
description:
- Configures the network settings for the grid member.
required: true
suboptions:
address:
description:
- The IPv4 Address of the Grid Member
subnet_mask:
description:
- The subnet mask for the Grid Member
gateway:
description:
- The default gateway for the Grid Member
ipv6_setting:
description:
- Configures the IPv6 settings for the grid member.
required: true
suboptions:
virtual_ip:
description:
- The IPv6 Address of the Grid Member
cidr_prefix:
description:
- The IPv6 CIDR prefix for the Grid Member
gateway:
description:
- The gateway address for the Grid Member
config_addr_type:
description:
- Address configuration type (IPV4/IPV6/BOTH)
default: IPV4
comment:
description:
- A descriptive comment of the Grid member.
extattrs:
description:
- Extensible attributes associated with the object.
enable_ha:
description:
- If set to True, the member has two physical nodes (HA pair).
type: bool
router_id:
description:
- Virtual router identifier. Provide this ID if "ha_enabled" is set to "true". This is a unique VRID number (from 1 to 255) for the local subnet.
lan2_enabled:
description:
- When set to "true", the LAN2 port is enabled as an independent port or as a port for failover purposes.
type: bool
lan2_port_setting:
description:
- Settings for the Grid member LAN2 port if 'lan2_enabled' is set to "true".
suboptions:
enabled:
description:
- If set to True, then it has its own IP settings.
type: bool
network_setting:
description:
- If the 'enable' field is set to True, this defines IPv4 network settings for LAN2.
suboptions:
address:
description:
- The IPv4 Address of LAN2
subnet_mask:
description:
- The subnet mask of LAN2
gateway:
description:
- The default gateway of LAN2
v6_network_setting:
description:
- If the 'enable' field is set to True, this defines IPv6 network settings for LAN2.
suboptions:
virtual_ip:
description:
- The IPv6 Address of LAN2
cidr_prefix:
description:
- The IPv6 CIDR prefix of LAN2
gateway:
description:
- The gateway address of LAN2
platform:
description:
- Configures the Hardware Platform.
default: INFOBLOX
node_info:
description:
- Configures the node information list with detailed status report on the operations of the Grid Member.
suboptions:
lan2_physical_setting:
description:
- Physical port settings for the LAN2 interface.
suboptions:
auto_port_setting_enabled:
description:
- Enable or disalbe the auto port setting.
type: bool
duplex:
description:
- The port duplex; if speed is 1000, duplex must be FULL.
speed:
description:
- The port speed; if speed is 1000, duplex is FULL.
lan_ha_port_setting:
description:
- LAN/HA port settings for the node.
suboptions:
ha_ip_address:
description:
- HA IP address.
ha_port_setting:
description:
- Physical port settings for the HA interface.
suboptions:
auto_port_setting_enabled:
description:
- Enable or disalbe the auto port setting.
type: bool
duplex:
description:
- The port duplex; if speed is 1000, duplex must be FULL.
speed:
description:
- The port speed; if speed is 1000, duplex is FULL.
lan_port_setting:
description:
- Physical port settings for the LAN interface.
suboptions:
auto_port_setting_enabled:
description:
- Enable or disalbe the auto port setting.
type: bool
duplex:
description:
- The port duplex; if speed is 1000, duplex must be FULL.
speed:
description:
- The port speed; if speed is 1000, duplex is FULL.
mgmt_ipv6addr:
description:
- Public IPv6 address for the LAN1 interface.
mgmt_lan:
description:
- Public IPv4 address for the LAN1 interface.
mgmt_network_setting:
description:
- Network settings for the MGMT port of the node.
suboptions:
address:
description:
- The IPv4 Address of MGMT
subnet_mask:
description:
- The subnet mask of MGMT
gateway:
description:
- The default gateway of MGMT
v6_mgmt_network_setting:
description:
- The network settings for the IPv6 MGMT port of the node.
suboptions:
virtual_ip:
description:
- The IPv6 Address of MGMT
cidr_prefix:
description:
- The IPv6 CIDR prefix of MGMT
gateway:
description:
- The gateway address of MGMT
mgmt_port_setting:
description:
- Settings for the member MGMT port.
suboptions:
enabled:
description:
- Determines if MGMT port settings should be enabled.
type: bool
security_access_enabled:
description:
- Determines if security access on the MGMT port is enabled or not.
type: bool
vpn_enabled:
description:
- Determines if VPN on the MGMT port is enabled or not.
type: bool
upgrade_group:
description:
- The name of the upgrade group to which this Grid member belongs.
default: Default
use_syslog_proxy_setting:
description:
- Use flag for external_syslog_server_enable , syslog_servers, syslog_proxy_setting, syslog_size
type: bool
external_syslog_server_enable:
description:
- Determines if external syslog servers should be enabled
type: bool
syslog_servers:
description:
- The list of external syslog servers.
suboptions:
address:
description:
- The server address.
category_list:
description:
- The list of all syslog logging categories.
connection_type:
description:
- The connection type for communicating with this server.(STCP/TCP?UDP)
default: UDP
local_interface:
description:
- The local interface through which the appliance sends syslog messages to the syslog server.(ANY/LAN/MGMT)
default: ANY
message_node_id:
description:
- Identify the node in the syslog message. (HOSTNAME/IP_HOSTNAME/LAN/MGMT)
default: LAN
message_source:
description:
- The source of syslog messages to be sent to the external syslog server.
default: ANY
only_category_list:
description:
- The list of selected syslog logging categories. The appliance forwards syslog messages that belong to the selected categories.
type: bool
port:
description:
- The port this server listens on.
default: 514
severity:
description:
- The severity filter. The appliance sends log messages of the specified severity and above to the external syslog server.
default: DEBUG
pre_provisioning:
description:
- Pre-provisioning information.
suboptions:
hardware_info:
description:
- An array of structures that describe the hardware being pre-provisioned.
suboptions:
hwmodel:
description:
- Hardware model
hwtype:
description:
- Hardware type.
licenses:
description:
- An array of license types.
create_token:
description:
- Flag for initiating a create token request for pre-provisioned members.
type: bool
default: False
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: add a member to the grid with IPv4 address
nios_member:
host_name: member01.localdomain
vip_setting:
- address: 192.168.1.100
subnet_mask: 255.255.255.0
gateway: 192.168.1.1
config_addr_type: IPV4
platform: VNIOS
comment: "Created by Ansible"
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a HA member to the grid
nios_member:
host_name: memberha.localdomain
vip_setting:
- address: 192.168.1.100
subnet_mask: 255.255.255.0
gateway: 192.168.1.1
config_addr_type: IPV4
platform: VNIOS
enable_ha: true
router_id: 150
node_info:
- lan_ha_port_setting:
- ha_ip_address: 192.168.1.70
mgmt_lan: 192.168.1.80
- lan_ha_port_setting:
- ha_ip_address: 192.168.1.71
mgmt_lan: 192.168.1.81
comment: "Created by Ansible"
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update the member with pre-provisioning details specified
nios_member:
name: member01.localdomain
pre_provisioning:
- hardware_info:
- hwmodel: IB-VM-820
hwtype: IB-VNIOS
licenses:
- dns
- dhcp
- enterprise
- vnios
comment: "Updated by Ansible"
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove the member
nios_member:
name: member01.localdomain
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_MEMBER
def main():
''' Main entry point for module execution
'''
ipv4_spec = dict(
address=dict(),
subnet_mask=dict(),
gateway=dict(),
)
ipv6_spec = dict(
virtual_ip=dict(),
cidr_prefix=dict(type='int'),
gateway=dict(),
)
port_spec = dict(
auto_port_setting_enabled=dict(type='bool'),
duplex=dict(),
speed=dict(),
)
lan2_port_spec = dict(
enabled=dict(type='bool'),
network_setting=dict(type='list', elements='dict', options=ipv4_spec),
v6_network_setting=dict(type='list', elements='dict', options=ipv6_spec),
)
ha_port_spec = dict(
ha_ip_address=dict(),
ha_port_setting=dict(type='list', elements='dict', options=port_spec),
lan_port_setting=dict(type='list', elements='dict', options=port_spec),
mgmt_lan=dict(),
mgmt_ipv6addr=dict(),
)
node_spec = dict(
lan2_physical_setting=dict(type='list', elements='dict', options=port_spec),
lan_ha_port_setting=dict(type='list', elements='dict', options=ha_port_spec),
mgmt_network_setting=dict(type='list', elements='dict', options=ipv4_spec),
v6_mgmt_network_setting=dict(type='list', elements='dict', options=ipv6_spec),
)
mgmt_port_spec = dict(
enabled=dict(type='bool'),
security_access_enabled=dict(type='bool'),
vpn_enabled=dict(type='bool'),
)
syslog_spec = dict(
address=dict(),
category_list=dict(type='list'),
connection_type=dict(default='UDP'),
local_interface=dict(default='ANY'),
message_node_id=dict(default='LAN'),
message_source=dict(default='ANY'),
only_category_list=dict(type='bool'),
port=dict(type='int', default=514),
severity=dict(default='DEBUG'),
)
hw_spec = dict(
hwmodel=dict(),
hwtype=dict(),
)
pre_prov_spec = dict(
hardware_info=dict(type='list', elements='dict', options=hw_spec),
licenses=dict(type='list'),
)
ib_spec = dict(
host_name=dict(required=True, aliases=['name'], ib_req=True),
vip_setting=dict(type='list', elements='dict', options=ipv4_spec),
ipv6_setting=dict(type='list', elements='dict', options=ipv6_spec),
config_addr_type=dict(default='IPV4'),
comment=dict(),
enable_ha=dict(type='bool', default=False),
router_id=dict(type='int'),
lan2_enabled=dict(type='bool', default=False),
lan2_port_setting=dict(type='list', elements='dict', options=lan2_port_spec),
platform=dict(default='INFOBLOX'),
node_info=dict(type='list', elements='dict', options=node_spec),
mgmt_port_setting=dict(type='list', elements='dict', options=mgmt_port_spec),
upgrade_group=dict(default='Default'),
use_syslog_proxy_setting=dict(type='bool'),
external_syslog_server_enable=dict(type='bool'),
syslog_servers=dict(type='list', elements='dict', options=syslog_spec),
pre_provisioning=dict(type='list', elements='dict', options=pre_prov_spec),
extattrs=dict(type='dict'),
create_token=dict(type='bool', default=False),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_MEMBER, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,158 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_mx_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS MX records
description:
- Adds and/or removes instances of MX record objects from
Infoblox NIOS servers. This module manages NIOS C(record:mx) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this a record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
mail_exchanger:
description:
- Configures the mail exchanger FQDN for this MX record.
required: true
aliases:
- mx
preference:
description:
- Configures the preference (0-65535) for this MX record.
required: true
ttl:
description:
- Configures the TTL to be associated with this host record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure an MX record
nios_mx_record:
name: ansible.com
mx: mailhost.ansible.com
preference: 0
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing MX record
nios_mx_record:
name: ansible.com
mx: mailhost.ansible.com
preference: 0
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove an MX record from the system
nios_mx_record:
name: ansible.com
mx: mailhost.ansible.com
preference: 0
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_MX_RECORD
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
mail_exchanger=dict(aliases=['mx'], ib_req=True),
preference=dict(type='int', ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_MX_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,192 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_naptr_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS NAPTR records
description:
- Adds and/or removes instances of NAPTR record objects from
Infoblox NIOS servers. This module manages NIOS C(record:naptr) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox_client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this a record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
order:
description:
- Configures the order (0-65535) for this NAPTR record. This parameter
specifies the order in which the NAPTR rules are applied when
multiple rules are present.
required: true
preference:
description:
- Configures the preference (0-65535) for this NAPTR record. The
preference field determines the order NAPTR records are processed
when multiple records with the same order parameter are present.
required: true
replacement:
description:
- Configures the replacement field for this NAPTR record.
For nonterminal NAPTR records, this field specifies the
next domain name to look up.
required: true
services:
description:
- Configures the services field (128 characters maximum) for this
NAPTR record. The services field contains protocol and service
identifiers, such as "http+E2U" or "SIPS+D2T".
required: false
flags:
description:
- Configures the flags field for this NAPTR record. These control the
interpretation of the fields for an NAPTR record object. Supported
values for the flags field are "U", "S", "P" and "A".
required: false
regexp:
description:
- Configures the regexp field for this NAPTR record. This is the
regular expression-based rewriting rule of the NAPTR record. This
should be a POSIX compliant regular expression, including the
substitution rule and flags. Refer to RFC 2915 for the field syntax
details.
required: false
ttl:
description:
- Configures the TTL to be associated with this NAPTR record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a NAPTR record
nios_naptr_record:
name: '*.subscriber-100.ansiblezone.com'
order: 1000
preference: 10
replacement: replacement1.network.ansiblezone.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing NAPTR record
nios_naptr_record:
name: '*.subscriber-100.ansiblezone.com'
order: 1000
preference: 10
replacement: replacement1.network.ansiblezone.com
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a NAPTR record from the system
nios_naptr_record:
name: '*.subscriber-100.ansiblezone.com'
order: 1000
preference: 10
replacement: replacement1.network.ansiblezone.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
order=dict(type='int', ib_req=True),
preference=dict(type='int', ib_req=True),
replacement=dict(ib_req=True),
services=dict(),
flags=dict(),
regexp=dict(),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run('record:naptr', ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,298 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_network
author: "Peter Sprygada (@privateip)"
short_description: Configure Infoblox NIOS network object
description:
- Adds and/or removes instances of network objects from
Infoblox NIOS servers. This module manages NIOS C(network) objects
using the Infoblox WAPI interface over REST.
- Supports both IPV4 and IPV6 internet protocols
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
network:
description:
- Specifies the network to add or remove from the system. The value
should use CIDR notation.
required: true
aliases:
- name
- cidr
network_view:
description:
- Configures the name of the network view to associate with this
configured instance.
required: true
default: default
options:
description:
- Configures the set of DHCP options to be included as part of
the configured network instance. This argument accepts a list
of values (see suboptions). When configuring suboptions at
least one of C(name) or C(num) must be specified.
suboptions:
name:
description:
- The name of the DHCP option to configure. The standard options are
C(router), C(router-templates), C(domain-name-servers), C(domain-name),
C(broadcast-address), C(broadcast-address-offset), C(dhcp-lease-time),
and C(dhcp6.name-servers).
num:
description:
- The number of the DHCP option to configure
value:
description:
- The value of the DHCP option specified by C(name)
required: true
use_option:
description:
- Only applies to a subset of options (see NIOS API documentation)
type: bool
default: 'yes'
vendor_class:
description:
- The name of the space this DHCP option is associated to
default: DHCP
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
container:
description:
- If set to true it'll create the network container to be added or removed
from the system.
type: bool
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a network ipv4
nios_network:
network: 192.168.10.0/24
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a network ipv6
nios_network:
network: fe80::/64
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: set dhcp options for a network ipv4
nios_network:
network: 192.168.10.0/24
comment: this is a test comment
options:
- name: domain-name
value: ansible.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a network ipv4
nios_network:
network: 192.168.10.0/24
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a ipv4 network container
nios_network:
network: 192.168.10.0/24
container: true
comment: test network container
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a ipv6 network container
nios_network:
network: fe80::/64
container: true
comment: test network container
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove a ipv4 network container
nios_network:
networkr: 192.168.10.0/24
container: true
comment: test network container
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import validate_ip_address, validate_ip_v6_address
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_IPV4_NETWORK, NIOS_IPV6_NETWORK
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_IPV4_NETWORK_CONTAINER, NIOS_IPV6_NETWORK_CONTAINER
def options(module):
''' Transforms the module argument into a valid WAPI struct
This function will transform the options argument into a structure that
is a valid WAPI structure in the format of:
{
name: <value>,
num: <value>,
value: <value>,
use_option: <value>,
vendor_class: <value>
}
It will remove any options that are set to None since WAPI will error on
that condition. It will also verify that either `name` or `num` is
set in the structure but does not validate the values are equal.
The remainder of the value validation is performed by WAPI
'''
options = list()
for item in module.params['options']:
opt = dict([(k, v) for k, v in iteritems(item) if v is not None])
if 'name' not in opt and 'num' not in opt:
module.fail_json(msg='one of `name` or `num` is required for option value')
options.append(opt)
return options
def check_ip_addr_type(obj_filter, ib_spec):
'''This function will check if the argument ip is type v4/v6 and return appropriate infoblox
network/networkcontainer type
'''
ip = obj_filter['network']
if 'container' in obj_filter and obj_filter['container']:
check_ip = ip.split('/')
del ib_spec['container'] # removing the container key from post arguments
del ib_spec['options'] # removing option argument as for network container it's not supported
if validate_ip_address(check_ip[0]):
return NIOS_IPV4_NETWORK_CONTAINER, ib_spec
elif validate_ip_v6_address(check_ip[0]):
return NIOS_IPV6_NETWORK_CONTAINER, ib_spec
else:
check_ip = ip.split('/')
del ib_spec['container'] # removing the container key from post arguments
if validate_ip_address(check_ip[0]):
return NIOS_IPV4_NETWORK, ib_spec
elif validate_ip_v6_address(check_ip[0]):
return NIOS_IPV6_NETWORK, ib_spec
def check_vendor_specific_dhcp_option(module, ib_spec):
'''This function will check if the argument dhcp option belongs to vendor-specific and if yes then will remove
use_options flag which is not supported with vendor-specific dhcp options.
'''
for key, value in iteritems(ib_spec):
if isinstance(module.params[key], list):
temp_dict = module.params[key][0]
if 'num' in temp_dict:
if temp_dict['num'] in (43, 124, 125):
del module.params[key][0]['use_option']
return ib_spec
def main():
''' Main entry point for module execution
'''
option_spec = dict(
# one of name or num is required; enforced by the function options()
name=dict(),
num=dict(type='int'),
value=dict(required=True),
use_option=dict(type='bool', default=True),
vendor_class=dict(default='DHCP')
)
ib_spec = dict(
network=dict(required=True, aliases=['name', 'cidr'], ib_req=True),
network_view=dict(default='default', ib_req=True),
options=dict(type='list', elements='dict', options=option_spec, transform=options),
extattrs=dict(type='dict'),
comment=dict(),
container=dict(type='bool', ib_req=True)
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
# to get the argument ipaddr
obj_filter = dict([(k, module.params[k]) for k, v in iteritems(ib_spec) if v.get('ib_req')])
network_type, ib_spec = check_ip_addr_type(obj_filter, ib_spec)
wapi = WapiModule(module)
# to check for vendor specific dhcp option
ib_spec = check_vendor_specific_dhcp_option(module, ib_spec)
result = wapi.run(network_type, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,133 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_network_view
author: "Peter Sprygada (@privateip)"
short_description: Configure Infoblox NIOS network views
description:
- Adds and/or removes instances of network view objects from
Infoblox NIOS servers. This module manages NIOS C(networkview) objects
using the Infoblox WAPI interface over REST.
- Updates instances of network view object from Infoblox NIOS servers.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system. User can also update the hostname as it is possible
to pass a dict containing I(new_name), I(old_name). See examples.
required: true
aliases:
- network_view
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a new network view
nios_network_view:
name: ansible
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update the comment for network view
nios_network_view:
name: ansible
comment: this is an example comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove the network view
nios_network_view:
name: ansible
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update a existing network view
nios_network_view:
name: {new_name: ansible-new, old_name: ansible}
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_NETWORK_VIEW
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, aliases=['network_view'], ib_req=True),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_NETWORK_VIEW, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,350 @@
#!/usr/bin/python
# -*- 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_nsgroup
short_description: Configure InfoBlox DNS Nameserver Groups
extends_documentation_fragment:
- community.general.nios
author:
- Erich Birngruber (@ebirn)
- Sumit Jaiswal (@sjaiswal)
description:
- Adds and/or removes nameserver groups form Infoblox NIOS servers.
This module manages NIOS C(nsgroup) objects using the Infoblox. WAPI interface over REST.
requirements:
- infoblox_client
options:
name:
description:
- Specifies the name of the NIOS nameserver group to be managed.
required: true
grid_primary:
description:
- This host is to be used as primary server in this nameserver group. It must be a grid member.
This option is required when setting I(use_external_primaries) to C(false).
suboptions:
name:
description:
- Provide the name of the grid member to identify the host.
required: true
enable_preferred_primaries:
description:
- This flag represents whether the preferred_primaries field values of this member are used (see Infoblox WAPI docs).
default: false
type: bool
grid_replicate:
description:
- Use DNS zone transfers if set to C(True) or ID Grid Replication if set to C(False).
type: bool
default: false
lead:
description:
- This flag controls if the grid lead secondary nameserver performs zone transfers to non lead secondaries.
type: bool
default: false
stealth:
description:
- Configure the external nameserver as stealth server (without NS record) in the zones.
type: bool
default: false
grid_secondaries:
description:
- Configures the list of grid member hosts that act as secondary nameservers.
This option is required when setting I(use_external_primaries) to C(true).
suboptions:
name:
description:
- Provide the name of the grid member to identify the host.
required: true
enable_preferred_primaries:
description:
- This flag represents whether the preferred_primaries field values of this member are used (see Infoblox WAPI docs).
default: false
type: bool
grid_replicate:
description:
- Use DNS zone transfers if set to C(True) or ID Grid Replication if set to C(False)
type: bool
default: false
lead:
description:
- This flag controls if the grid lead secondary nameserver performs zone transfers to non lead secondaries.
type: bool
default: false
stealth:
description:
- Configure the external nameserver as stealth server (without NS record) in the zones.
type: bool
default: false
preferred_primaries:
description:
- Provide a list of elements like in I(external_primaries) to set the precedence of preferred primary nameservers.
is_grid_default:
description:
- If set to C(True) this nsgroup will become the default nameserver group for new zones.
type: bool
required: false
default: false
use_external_primary:
description:
- This flag controls whether the group is using an external primary nameserver.
Note that modification of this field requires passing values for I(grid_secondaries) and I(external_primaries).
type: bool
required: false
default: false
external_primaries:
description:
- Configures a list of external nameservers (non-members of the grid).
This option is required when setting I(use_external_primaries) to C(true).
suboptions:
address:
description:
- Configures the IP address of the external nameserver
required: true
name:
description:
- Set a label for the external nameserver
required: true
stealth:
description:
- Configure the external nameserver as stealth server (without NS record) in the zones.
type: bool
default: false
tsig_key_name:
description:
- Sets a label for the I(tsig_key) value
tsig_key_alg:
description:
- Provides the algorithm used for the I(tsig_key) in use.
choices: ['HMAC-MD5', 'HMAC-SHA256']
default: 'HMAC-MD5'
tsig_key:
description:
- Set a DNS TSIG key for the nameserver to secure zone transfers (AFXRs).
required: false
external_secondaries:
description:
- Allows to provide a list of external secondary nameservers, that are not members of the grid.
suboptions:
address:
description:
- Configures the IP address of the external nameserver
required: true
name:
description:
- Set a label for the external nameserver
required: true
stealth:
description:
- Configure the external nameserver as stealth server (without NS record) in the zones.
type: bool
default: false
tsig_key_name:
description:
- Sets a label for the I(tsig_key) value
tsig_key_alg:
description:
- Provides the algorithm used for the I(tsig_key) in use.
choices: ['HMAC-MD5', 'HMAC-SHA256']
default: 'HMAC-MD5'
tsig_key:
description:
- Set a DNS TSIG key for the nameserver to secure zone transfers (AFXRs).
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
required: false
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
required: false
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
choices: [present, absent]
default: present
'''
EXAMPLES = '''
- name: create simple infoblox nameserver group
nios_nsgroup:
name: my-simple-group
comment: "this is a simple nameserver group"
grid_primary:
- name: infoblox-test.example.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: create infoblox nameserver group with external primaries
nios_nsgroup:
name: my-example-group
use_external_primary: true
comment: "this is my example nameserver group"
external_primaries: "{{ ext_nameservers }}"
grid_secondaries:
- name: infoblox-test.example.com
lead: True
preferred_primaries: "{{ ext_nameservers }}"
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: delete infoblox nameserver group
nios_nsgroup:
name: my-simple-group
comment: "this is a simple nameserver group"
grid_primary:
- name: infoblox-test.example.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_NSGROUP
# from infoblox documentation
# Fields List
# Field Type Req R/O Base Search
# comment String N N Y : = ~
# extattrs Extattr N N N ext
# external_primaries [struct] N N N N/A
# external_secondaries [struct] N N N N/A
# grid_primary [struct] N N N N/A
# grid_secondaries [struct] N N N N/A
# is_grid_default Bool N N N N/A
# is_multimaster Bool N Y N N/A
# name String Y N Y : = ~
# use_external_primary Bool N N N N/A
def main():
'''entrypoint for module execution.'''
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent']),
)
# cleanup tsig fields
def clean_tsig(ext):
if 'tsig_key' in ext and not ext['tsig_key']:
del ext['tsig_key']
if 'tsig_key' not in ext and 'tsig_key_name' in ext and not ext['tsig_key_name']:
del ext['tsig_key_name']
if 'tsig_key' not in ext and 'tsig_key_alg' in ext:
del ext['tsig_key_alg']
def clean_grid_member(member):
if member['preferred_primaries']:
for ext in member['preferred_primaries']:
clean_tsig(ext)
if member['enable_preferred_primaries'] is False:
del member['enable_preferred_primaries']
del member['preferred_primaries']
if member['lead'] is False:
del member['lead']
if member['grid_replicate'] is False:
del member['grid_replicate']
def ext_primaries_transform(module):
if module.params['external_primaries']:
for ext in module.params['external_primaries']:
clean_tsig(ext)
return module.params['external_primaries']
def ext_secondaries_transform(module):
if module.params['external_secondaries']:
for ext in module.params['external_secondaries']:
clean_tsig(ext)
return module.params['external_secondaries']
def grid_primary_preferred_transform(module):
for member in module.params['grid_primary']:
clean_grid_member(member)
return module.params['grid_primary']
def grid_secondaries_preferred_primaries_transform(module):
for member in module.params['grid_secondaries']:
clean_grid_member(member)
return module.params['grid_secondaries']
extserver_spec = dict(
address=dict(required=True, ib_req=True),
name=dict(required=True, ib_req=True),
stealth=dict(type='bool', default=False),
tsig_key=dict(),
tsig_key_alg=dict(choices=['HMAC-MD5', 'HMAC-SHA256'], default='HMAC-MD5'),
tsig_key_name=dict(required=True)
)
memberserver_spec = dict(
name=dict(required=True, ib_req=True),
enable_preferred_primaries=dict(type='bool', default=False),
grid_replicate=dict(type='bool', default=False),
lead=dict(type='bool', default=False),
preferred_primaries=dict(type='list', elements='dict', options=extserver_spec, default=[]),
stealth=dict(type='bool', default=False),
)
ib_spec = dict(
name=dict(required=True, ib_req=True),
grid_primary=dict(type='list', elements='dict', options=memberserver_spec,
transform=grid_primary_preferred_transform),
grid_secondaries=dict(type='list', elements='dict', options=memberserver_spec,
transform=grid_secondaries_preferred_primaries_transform),
external_primaries=dict(type='list', elements='dict', options=extserver_spec, transform=ext_primaries_transform),
external_secondaries=dict(type='list', elements='dict', options=extserver_spec,
transform=ext_secondaries_transform),
is_grid_default=dict(type='bool', default=False),
use_external_primary=dict(type='bool', default=False),
extattrs=dict(),
comment=dict(),
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_NSGROUP, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,160 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_ptr_record
author: "Trebuchet Clement (@clementtrebuchet)"
short_description: Configure Infoblox NIOS PTR records
description:
- Adds and/or removes instances of PTR record objects from
Infoblox NIOS servers. This module manages NIOS C(record:ptr) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox_client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- The name of the DNS PTR record in FQDN format to add or remove from
the system.
The field is required only for an PTR object in Forward Mapping Zone.
required: false
view:
description:
- Sets the DNS view to associate this a record with. The DNS
view must already be configured on the system
required: false
aliases:
- dns_view
ipv4addr:
description:
- The IPv4 Address of the record. Mutually exclusive with the ipv6addr.
required: true
aliases:
- ipv4
ipv6addr:
description:
- The IPv6 Address of the record. Mutually exclusive with the ipv4addr.
required: true
aliases:
- ipv6
ptrdname:
description:
- The domain name of the DNS PTR record in FQDN format.
required: true
ttl:
description:
- Time To Live (TTL) value for the record.
A 32-bit unsigned integer that represents the duration, in seconds, that the record is valid (cached).
Zero indicates that the record should not be cached.
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance. Maximum 256 characters.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: Create a PTR Record
nios_ptr_record:
ipv4: 192.168.10.1
ptrdname: host.ansible.com
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: Delete a PTR Record
nios_ptr_record:
ipv4: 192.168.10.1
ptrdname: host.ansible.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_PTR_RECORD
def main():
# Module entry point
ib_spec = dict(
name=dict(required=False),
view=dict(aliases=['dns_view'], ib_req=True),
ipv4addr=dict(aliases=['ipv4'], ib_req=True),
ipv6addr=dict(aliases=['ipv6'], ib_req=True),
ptrdname=dict(ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
mutually_exclusive = [('ipv4addr', 'ipv6addr')]
required_one_of = [
['ipv4addr', 'ipv6addr']
]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
required_one_of=required_one_of)
if module.params['ipv4addr']:
del ib_spec['ipv6addr']
elif module.params['ipv6addr']:
del ib_spec['ipv4addr']
wapi = WapiModule(module)
result = wapi.run(NIOS_PTR_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,172 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_srv_record
author: "Blair Rampling (@brampling)"
short_description: Configure Infoblox NIOS SRV records
description:
- Adds and/or removes instances of SRV record objects from
Infoblox NIOS servers. This module manages NIOS C(record:srv) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this a record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
port:
description:
- Configures the port (0-65535) of this SRV record.
required: true
priority:
description:
- Configures the priority (0-65535) for this SRV record.
required: true
target:
description:
- Configures the target FQDN for this SRV record.
required: true
weight:
description:
- Configures the weight (0-65535) for this SRV record.
required: true
ttl:
description:
- Configures the TTL to be associated with this host record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure an SRV record
nios_srv_record:
name: _sip._tcp.service.ansible.com
port: 5080
priority: 10
target: service1.ansible.com
weight: 10
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: add a comment to an existing SRV record
nios_srv_record:
name: _sip._tcp.service.ansible.com
port: 5080
priority: 10
target: service1.ansible.com
weight: 10
comment: this is a test comment
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove an SRV record from the system
nios_srv_record:
name: _sip._tcp.service.ansible.com
port: 5080
priority: 10
target: service1.ansible.com
weight: 10
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_SRV_RECORD
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
port=dict(type='int', ib_req=True),
priority=dict(type='int', ib_req=True),
target=dict(ib_req=True),
weight=dict(type='int', ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run(NIOS_SRV_RECORD, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,135 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_txt_record
author: "Corey Wanless (@coreywan)"
short_description: Configure Infoblox NIOS txt records
description:
- Adds and/or removes instances of txt record objects from
Infoblox NIOS servers. This module manages NIOS C(record:txt) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox_client
extends_documentation_fragment:
- community.general.nios
options:
name:
description:
- Specifies the fully qualified hostname to add or remove from
the system
required: true
view:
description:
- Sets the DNS view to associate this tst record with. The DNS
view must already be configured on the system
required: true
default: default
aliases:
- dns_view
text:
description:
- Text associated with the record. It can contain up to 255 bytes
per substring, up to a total of 512 bytes. To enter leading,
trailing, or embedded spaces in the text, add quotes around the
text to preserve the spaces.
required: true
ttl:
description:
- Configures the TTL to be associated with this tst record
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: Ensure a text Record Exists
nios_txt_record:
name: fqdn.txt.record.com
text: mytext
state: present
view: External
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
- name: Ensure a text Record does not exist
nios_txt_record:
name: fqdn.txt.record.com
text: mytext
state: absent
view: External
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
def main():
''' Main entry point for module execution
'''
ib_spec = dict(
name=dict(required=True, ib_req=True),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
text=dict(ib_req=True),
ttl=dict(type='int'),
extattrs=dict(type='dict'),
comment=dict(),
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
wapi = WapiModule(module)
result = wapi.run('record:txt', ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,226 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'certified'}
DOCUMENTATION = '''
---
module: nios_zone
author: "Peter Sprygada (@privateip)"
short_description: Configure Infoblox NIOS DNS zones
description:
- Adds and/or removes instances of DNS zone objects from
Infoblox NIOS servers. This module manages NIOS C(zone_auth) objects
using the Infoblox WAPI interface over REST.
requirements:
- infoblox-client
extends_documentation_fragment:
- community.general.nios
options:
fqdn:
description:
- Specifies the qualified domain name to either add or remove from
the NIOS instance based on the configured C(state) value.
required: true
aliases:
- name
view:
description:
- Configures the DNS view name for the configured resource. The
specified DNS zone must already exist on the running NIOS instance
prior to configuring zones.
required: true
default: default
aliases:
- dns_view
grid_primary:
description:
- Configures the grid primary servers for this zone.
suboptions:
name:
description:
- The name of the grid primary server
grid_secondaries:
description:
- Configures the grid secondary servers for this zone.
suboptions:
name:
description:
- The name of the grid secondary server
ns_group:
description:
- Configures the name server group for this zone. Name server group is
mutually exclusive with grid primary and grid secondaries.
restart_if_needed:
description:
- If set to true, causes the NIOS DNS service to restart and load the
new zone configuration
type: bool
zone_format:
description:
- Create an authorative Reverse-Mapping Zone which is an area of network
space for which one or more name servers-primary and secondary-have the
responsibility to respond to address-to-name queries. It supports
reverse-mapping zones for both IPv4 and IPv6 addresses.
default: FORWARD
extattrs:
description:
- Allows for the configuration of Extensible Attributes on the
instance of the object. This argument accepts a set of key / value
pairs for configuration.
comment:
description:
- Configures a text string comment to be associated with the instance
of this object. The provided text string will be configured on the
object instance.
state:
description:
- Configures the intended state of the instance of the object on
the NIOS server. When this value is set to C(present), the object
is configured on the device and when this value is set to C(absent)
the value is removed (if necessary) from the device.
default: present
choices:
- present
- absent
'''
EXAMPLES = '''
- name: configure a zone on the system using grid primary and secondaries
nios_zone:
name: ansible.com
grid_primary:
- name: gridprimary.grid.com
grid_secondaries:
- name: gridsecondary1.grid.com
- name: gridsecondary2.grid.com
restart_if_needed: true
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a zone on the system using a name server group
nios_zone:
name: ansible.com
ns_group: examplensg
restart_if_needed: true
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a reverse mapping zone on the system using IPV4 zone format
nios_zone:
name: 10.10.10.0/24
zone_format: IPV4
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: configure a reverse mapping zone on the system using IPV6 zone format
nios_zone:
name: 100::1/128
zone_format: IPV6
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: update the comment and ext attributes for an existing zone
nios_zone:
name: ansible.com
comment: this is an example comment
extattrs:
Site: west-dc
state: present
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove the dns zone
nios_zone:
name: ansible.com
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
- name: remove the reverse mapping dns zone from the system with IPV4 zone format
nios_zone:
name: 10.10.10.0/24
zone_format: IPV4
state: absent
provider:
host: "{{ inventory_hostname_short }}"
username: admin
password: admin
connection: local
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiModule
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import NIOS_ZONE
def main():
''' Main entry point for module execution
'''
grid_spec = dict(
name=dict(required=True),
)
ib_spec = dict(
fqdn=dict(required=True, aliases=['name'], ib_req=True, update=False),
zone_format=dict(default='FORWARD', aliases=['zone_format'], ib_req=False),
view=dict(default='default', aliases=['dns_view'], ib_req=True),
grid_primary=dict(type='list', elements='dict', options=grid_spec),
grid_secondaries=dict(type='list', elements='dict', options=grid_spec),
ns_group=dict(),
restart_if_needed=dict(type='bool'),
extattrs=dict(type='dict'),
comment=dict()
)
argument_spec = dict(
provider=dict(required=True),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(ib_spec)
argument_spec.update(WapiModule.provider_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
mutually_exclusive=[
['ns_group', 'grid_primary'],
['ns_group', 'grid_secondaries']
])
wapi = WapiModule(module)
result = wapi.run(NIOS_ZONE, ib_spec)
module.exit_json(**result)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,474 @@
#!/usr/bin/python
# (c) 2016, Marcin Skarbek <github@skarbek.name>
# (c) 2016, Andreas Olsson <andreas@arrakis.se>
# (c) 2017, Loic Blot <loic.blot@unix-experience.fr>
#
# This module was ported from https://github.com/mskarbek/ansible-nsupdate
#
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: nsupdate
short_description: Manage DNS records.
description:
- Create, update and remove DNS records using DDNS updates
requirements:
- dnspython
author: "Loic Blot (@nerzhul)"
options:
state:
description:
- Manage DNS record.
choices: ['present', 'absent']
default: 'present'
server:
description:
- Apply DNS modification on this server, specified by IPv4 or IPv6 address.
required: true
port:
description:
- Use this TCP port when connecting to C(server).
default: 53
key_name:
description:
- Use TSIG key name to authenticate against DNS C(server)
key_secret:
description:
- Use TSIG key secret, associated with C(key_name), to authenticate against C(server)
key_algorithm:
description:
- Specify key algorithm used by C(key_secret).
choices: ['HMAC-MD5.SIG-ALG.REG.INT', 'hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256', 'hmac-sha384',
'hmac-sha512']
default: 'hmac-md5'
zone:
description:
- DNS record will be modified on this C(zone).
- When omitted DNS will be queried to attempt finding the correct zone.
- Starting with Ansible 2.7 this parameter is optional.
record:
description:
- Sets the DNS record to modify. When zone is omitted this has to be absolute (ending with a dot).
required: true
type:
description:
- Sets the record type.
default: 'A'
ttl:
description:
- Sets the record TTL.
default: 3600
value:
description:
- Sets the record value.
protocol:
description:
- Sets the transport protocol (TCP or UDP). TCP is the recommended and a more robust option.
default: 'tcp'
choices: ['tcp', 'udp']
'''
EXAMPLES = '''
- name: Add or modify ansible.example.org A to 192.168.1.1"
nsupdate:
key_name: "nsupdate"
key_secret: "+bFQtBCta7j2vWkjPkAFtgA=="
server: "10.1.1.1"
zone: "example.org"
record: "ansible"
value: "192.168.1.1"
- name: Add or modify ansible.example.org A to 192.168.1.1, 192.168.1.2 and 192.168.1.3"
nsupdate:
key_name: "nsupdate"
key_secret: "+bFQtBCta7j2vWkjPkAFtgA=="
server: "10.1.1.1"
zone: "example.org"
record: "ansible"
value: ["192.168.1.1", "192.168.1.2", "192.168.1.3"]
- name: Remove puppet.example.org CNAME
nsupdate:
key_name: "nsupdate"
key_secret: "+bFQtBCta7j2vWkjPkAFtgA=="
server: "10.1.1.1"
zone: "example.org"
record: "puppet"
type: "CNAME"
state: absent
- name: Add 1.1.168.192.in-addr.arpa. PTR for ansible.example.org
nsupdate:
key_name: "nsupdate"
key_secret: "+bFQtBCta7j2vWkjPkAFtgA=="
server: "10.1.1.1"
record: "1.1.168.192.in-addr.arpa."
type: "PTR"
value: "ansible.example.org."
state: present
- name: Remove 1.1.168.192.in-addr.arpa. PTR
nsupdate:
key_name: "nsupdate"
key_secret: "+bFQtBCta7j2vWkjPkAFtgA=="
server: "10.1.1.1"
record: "1.1.168.192.in-addr.arpa."
type: "PTR"
state: absent
'''
RETURN = '''
changed:
description: If module has modified record
returned: success
type: str
record:
description: DNS record
returned: success
type: str
sample: 'ansible'
ttl:
description: DNS record TTL
returned: success
type: int
sample: 86400
type:
description: DNS record type
returned: success
type: str
sample: 'CNAME'
value:
description: DNS record value(s)
returned: success
type: list
sample: '192.168.1.1'
zone:
description: DNS record zone
returned: success
type: str
sample: 'example.org.'
dns_rc:
description: dnspython return code
returned: always
type: int
sample: 4
dns_rc_str:
description: dnspython return code (string representation)
returned: always
type: str
sample: 'REFUSED'
'''
import traceback
from binascii import Error as binascii_error
from socket import error as socket_error
DNSPYTHON_IMP_ERR = None
try:
import dns.update
import dns.query
import dns.tsigkeyring
import dns.message
import dns.resolver
HAVE_DNSPYTHON = True
except ImportError:
DNSPYTHON_IMP_ERR = traceback.format_exc()
HAVE_DNSPYTHON = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
class RecordManager(object):
def __init__(self, module):
self.module = module
if module.params['key_name']:
try:
self.keyring = dns.tsigkeyring.from_text({
module.params['key_name']: module.params['key_secret']
})
except TypeError:
module.fail_json(msg='Missing key_secret')
except binascii_error as e:
module.fail_json(msg='TSIG key error: %s' % to_native(e))
else:
self.keyring = None
if module.params['key_algorithm'] == 'hmac-md5':
self.algorithm = 'HMAC-MD5.SIG-ALG.REG.INT'
else:
self.algorithm = module.params['key_algorithm']
if module.params['zone'] is None:
if module.params['record'][-1] != '.':
self.module.fail_json(msg='record must be absolute when omitting zone parameter')
self.zone = self.lookup_zone()
else:
self.zone = module.params['zone']
if self.zone[-1] != '.':
self.zone += '.'
if module.params['record'][-1] != '.':
self.fqdn = module.params['record'] + '.' + self.zone
else:
self.fqdn = module.params['record']
if self.module.params['type'].lower() == 'txt' and self.module.params['value'] is not None:
self.value = list(map(self.txt_helper, self.module.params['value']))
else:
self.value = self.module.params['value']
self.dns_rc = 0
def txt_helper(self, entry):
if entry[0] == '"' and entry[-1] == '"':
return entry
return '"{text}"'.format(text=entry)
def lookup_zone(self):
name = dns.name.from_text(self.module.params['record'])
while True:
query = dns.message.make_query(name, dns.rdatatype.SOA)
if self.keyring:
query.use_tsig(keyring=self.keyring, algorithm=self.algorithm)
try:
if self.module.params['protocol'] == 'tcp':
lookup = dns.query.tcp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
else:
lookup = dns.query.udp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature) as e:
self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, to_native(e)))
except (socket_error, dns.exception.Timeout) as e:
self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, to_native(e)))
if lookup.rcode() in [dns.rcode.SERVFAIL, dns.rcode.REFUSED]:
self.module.fail_json(msg='Zone lookup failure: \'%s\' will not respond to queries regarding \'%s\'.' % (
self.module.params['server'], self.module.params['record']))
try:
zone = lookup.authority[0].name
if zone == name:
return zone.to_text()
except IndexError:
pass
try:
name = name.parent()
except dns.name.NoParent:
self.module.fail_json(msg='Zone lookup of \'%s\' failed for unknown reason.' % (self.module.params['record']))
def __do_update(self, update):
response = None
try:
if self.module.params['protocol'] == 'tcp':
response = dns.query.tcp(update, self.module.params['server'], timeout=10, port=self.module.params['port'])
else:
response = dns.query.udp(update, self.module.params['server'], timeout=10, port=self.module.params['port'])
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature) as e:
self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, to_native(e)))
except (socket_error, dns.exception.Timeout) as e:
self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, to_native(e)))
return response
def create_or_update_record(self):
result = {'changed': False, 'failed': False}
exists = self.record_exists()
if exists in [0, 2]:
if self.module.check_mode:
self.module.exit_json(changed=True)
if exists == 0:
self.dns_rc = self.create_record()
if self.dns_rc != 0:
result['msg'] = "Failed to create DNS record (rc: %d)" % self.dns_rc
elif exists == 2:
self.dns_rc = self.modify_record()
if self.dns_rc != 0:
result['msg'] = "Failed to update DNS record (rc: %d)" % self.dns_rc
if self.dns_rc != 0:
result['failed'] = True
else:
result['changed'] = True
else:
result['changed'] = False
return result
def create_record(self):
update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
for entry in self.value:
try:
update.add(self.module.params['record'],
self.module.params['ttl'],
self.module.params['type'],
entry)
except AttributeError:
self.module.fail_json(msg='value needed when state=present')
except dns.exception.SyntaxError:
self.module.fail_json(msg='Invalid/malformed value')
response = self.__do_update(update)
return dns.message.Message.rcode(response)
def modify_record(self):
update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
update.delete(self.module.params['record'], self.module.params['type'])
for entry in self.value:
try:
update.add(self.module.params['record'],
self.module.params['ttl'],
self.module.params['type'],
entry)
except AttributeError:
self.module.fail_json(msg='value needed when state=present')
except dns.exception.SyntaxError:
self.module.fail_json(msg='Invalid/malformed value')
response = self.__do_update(update)
return dns.message.Message.rcode(response)
def remove_record(self):
result = {'changed': False, 'failed': False}
if self.record_exists() == 0:
return result
# Check mode and record exists, declared fake change.
if self.module.check_mode:
self.module.exit_json(changed=True)
update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
update.delete(self.module.params['record'], self.module.params['type'])
response = self.__do_update(update)
self.dns_rc = dns.message.Message.rcode(response)
if self.dns_rc != 0:
result['failed'] = True
result['msg'] = "Failed to delete record (rc: %d)" % self.dns_rc
else:
result['changed'] = True
return result
def record_exists(self):
update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
try:
update.present(self.module.params['record'], self.module.params['type'])
except dns.rdatatype.UnknownRdatatype as e:
self.module.fail_json(msg='Record error: {0}'.format(to_native(e)))
response = self.__do_update(update)
self.dns_rc = dns.message.Message.rcode(response)
if self.dns_rc == 0:
if self.module.params['state'] == 'absent':
return 1
for entry in self.value:
try:
update.present(self.module.params['record'], self.module.params['type'], entry)
except AttributeError:
self.module.fail_json(msg='value needed when state=present')
except dns.exception.SyntaxError:
self.module.fail_json(msg='Invalid/malformed value')
response = self.__do_update(update)
self.dns_rc = dns.message.Message.rcode(response)
if self.dns_rc == 0:
if self.ttl_changed():
return 2
else:
return 1
else:
return 2
else:
return 0
def ttl_changed(self):
query = dns.message.make_query(self.fqdn, self.module.params['type'])
if self.keyring:
query.use_tsig(keyring=self.keyring, algorithm=self.algorithm)
try:
if self.module.params['protocol'] == 'tcp':
lookup = dns.query.tcp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
else:
lookup = dns.query.udp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature) as e:
self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, to_native(e)))
except (socket_error, dns.exception.Timeout) as e:
self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, to_native(e)))
if lookup.rcode() != dns.rcode.NOERROR:
self.module.fail_json(msg='Failed to lookup TTL of existing matching record.')
current_ttl = lookup.answer[0].ttl
return current_ttl != self.module.params['ttl']
def main():
tsig_algs = ['HMAC-MD5.SIG-ALG.REG.INT', 'hmac-md5', 'hmac-sha1', 'hmac-sha224',
'hmac-sha256', 'hmac-sha384', 'hmac-sha512']
module = AnsibleModule(
argument_spec=dict(
state=dict(required=False, default='present', choices=['present', 'absent'], type='str'),
server=dict(required=True, type='str'),
port=dict(required=False, default=53, type='int'),
key_name=dict(required=False, type='str'),
key_secret=dict(required=False, type='str', no_log=True),
key_algorithm=dict(required=False, default='hmac-md5', choices=tsig_algs, type='str'),
zone=dict(required=False, default=None, type='str'),
record=dict(required=True, type='str'),
type=dict(required=False, default='A', type='str'),
ttl=dict(required=False, default=3600, type='int'),
value=dict(required=False, default=None, type='list'),
protocol=dict(required=False, default='tcp', choices=['tcp', 'udp'], type='str')
),
supports_check_mode=True
)
if not HAVE_DNSPYTHON:
module.fail_json(msg=missing_required_lib('dnspython'), exception=DNSPYTHON_IMP_ERR)
if len(module.params["record"]) == 0:
module.fail_json(msg='record cannot be empty.')
record = RecordManager(module)
result = {}
if module.params["state"] == 'absent':
result = record.remove_record()
elif module.params["state"] == 'present':
result = record.create_or_update_record()
result['dns_rc'] = record.dns_rc
result['dns_rc_str'] = dns.rcode.to_text(record.dns_rc)
if result['failed']:
module.fail_json(**result)
else:
result['record'] = dict(zone=record.zone,
record=module.params['record'],
type=module.params['type'],
ttl=module.params['ttl'],
value=record.value)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,314 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# copyright: (c) 2016, Loic Blot <loic.blot@unix-experience.fr>
# Sponsored by Infopro Digital. http://www.infopro-digital.com/
# Sponsored by E.T.A.I. http://www.etai.fr/
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: omapi_host
short_description: Setup OMAPI hosts.
description: Manage OMAPI hosts into compatible DHCPd servers
requirements:
- pypureomapi
author:
- Loic Blot (@nerzhul)
options:
state:
description:
- Create or remove OMAPI host.
type: str
required: true
choices: [ absent, present ]
hostname:
description:
- Sets the host lease hostname (mandatory if state=present).
type: str
aliases: [ name ]
host:
description:
- Sets OMAPI server host to interact with.
type: str
default: localhost
port:
description:
- Sets the OMAPI server port to interact with.
type: int
default: 7911
key_name:
description:
- Sets the TSIG key name for authenticating against OMAPI server.
type: str
required: true
key:
description:
- Sets the TSIG key content for authenticating against OMAPI server.
type: str
required: true
macaddr:
description:
- Sets the lease host MAC address.
type: str
required: true
ip:
description:
- Sets the lease host IP address.
type: str
statements:
description:
- Attach a list of OMAPI DHCP statements with host lease (without ending semicolon).
type: list
default: []
ddns:
description:
- Enable dynamic DNS updates for this host.
type: bool
default: no
'''
EXAMPLES = r'''
- name: Add a host using OMAPI
omapi_host:
key_name: defomapi
key: +bFQtBCta6j2vWkjPkNFtgA==
host: 10.98.4.55
macaddr: 44:dd:ab:dd:11:44
name: server01
ip: 192.168.88.99
ddns: yes
statements:
- filename "pxelinux.0"
- next-server 1.1.1.1
state: present
- name: Remove a host using OMAPI
omapi_host:
key_name: defomapi
key: +bFQtBCta6j2vWkjPkNFtgA==
host: 10.1.1.1
macaddr: 00:66:ab:dd:11:44
state: absent
'''
RETURN = r'''
lease:
description: dictionary containing host information
returned: success
type: complex
contains:
ip-address:
description: IP address, if there is.
returned: success
type: str
sample: '192.168.1.5'
hardware-address:
description: MAC address
returned: success
type: str
sample: '00:11:22:33:44:55'
hardware-type:
description: hardware type, generally '1'
returned: success
type: int
sample: 1
name:
description: hostname
returned: success
type: str
sample: 'mydesktop'
'''
import binascii
import socket
import struct
import traceback
PUREOMAPI_IMP_ERR = None
try:
from pypureomapi import Omapi, OmapiMessage, OmapiError, OmapiErrorNotFound
from pypureomapi import pack_ip, unpack_ip, pack_mac, unpack_mac
from pypureomapi import OMAPI_OP_STATUS, OMAPI_OP_UPDATE
pureomapi_found = True
except ImportError:
PUREOMAPI_IMP_ERR = traceback.format_exc()
pureomapi_found = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_bytes, to_native
class OmapiHostManager:
def __init__(self, module):
self.module = module
self.omapi = None
self.connect()
def connect(self):
try:
self.omapi = Omapi(self.module.params['host'], self.module.params['port'], self.module.params['key_name'],
self.module.params['key'])
except binascii.Error:
self.module.fail_json(msg="Unable to open OMAPI connection. 'key' is not a valid base64 key.")
except OmapiError as e:
self.module.fail_json(msg="Unable to open OMAPI connection. Ensure 'host', 'port', 'key' and 'key_name' "
"are valid. Exception was: %s" % to_native(e))
except socket.error as e:
self.module.fail_json(msg="Unable to connect to OMAPI server: %s" % to_native(e))
def get_host(self, macaddr):
msg = OmapiMessage.open(to_bytes("host", errors='surrogate_or_strict'))
msg.obj.append((to_bytes("hardware-address", errors='surrogate_or_strict'), pack_mac(macaddr)))
msg.obj.append((to_bytes("hardware-type", errors='surrogate_or_strict'), struct.pack("!I", 1)))
response = self.omapi.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
return None
return response
@staticmethod
def unpack_facts(obj):
result = dict(obj)
if 'hardware-address' in result:
result['hardware-address'] = unpack_mac(result['hardware-address'])
if 'ip-address' in result:
result['ip-address'] = unpack_ip(result['ip-address'])
if 'hardware-type' in result:
result['hardware-type'] = struct.unpack("!I", result['hardware-type'])
return result
def setup_host(self):
if self.module.params['hostname'] is None or len(self.module.params['hostname']) == 0:
self.module.fail_json(msg="name attribute could not be empty when adding or modifying host.")
msg = None
host_response = self.get_host(self.module.params['macaddr'])
# If host was not found using macaddr, add create message
if host_response is None:
msg = OmapiMessage.open(to_bytes('host', errors='surrogate_or_strict'))
msg.message.append(('create', struct.pack('!I', 1)))
msg.message.append(('exclusive', struct.pack('!I', 1)))
msg.obj.append(('hardware-address', pack_mac(self.module.params['macaddr'])))
msg.obj.append(('hardware-type', struct.pack('!I', 1)))
msg.obj.append(('name', self.module.params['hostname']))
if self.module.params['ip'] is not None:
msg.obj.append((to_bytes("ip-address", errors='surrogate_or_strict'), pack_ip(self.module.params['ip'])))
stmt_join = ""
if self.module.params['ddns']:
stmt_join += 'ddns-hostname "{0}"; '.format(self.module.params['hostname'])
try:
if len(self.module.params['statements']) > 0:
stmt_join += "; ".join(self.module.params['statements'])
stmt_join += "; "
except TypeError as e:
self.module.fail_json(msg="Invalid statements found: %s" % to_native(e))
if len(stmt_join) > 0:
msg.obj.append(('statements', stmt_join))
try:
response = self.omapi.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
self.module.fail_json(msg="Failed to add host, ensure authentication and host parameters "
"are valid.")
self.module.exit_json(changed=True, lease=self.unpack_facts(response.obj))
except OmapiError as e:
self.module.fail_json(msg="OMAPI error: %s" % to_native(e))
# Forge update message
else:
response_obj = self.unpack_facts(host_response.obj)
fields_to_update = {}
if to_bytes('ip-address', errors='surrogate_or_strict') not in response_obj or \
unpack_ip(response_obj[to_bytes('ip-address', errors='surrogate_or_strict')]) != self.module.params['ip']:
fields_to_update['ip-address'] = pack_ip(self.module.params['ip'])
# Name cannot be changed
if 'name' not in response_obj or response_obj['name'] != self.module.params['hostname']:
self.module.fail_json(msg="Changing hostname is not supported. Old was %s, new is %s. "
"Please delete host and add new." %
(response_obj['name'], self.module.params['hostname']))
"""
# It seems statements are not returned by OMAPI, then we cannot modify them at this moment.
if 'statements' not in response_obj and len(self.module.params['statements']) > 0 or \
response_obj['statements'] != self.module.params['statements']:
with open('/tmp/omapi', 'w') as fb:
for (k,v) in iteritems(response_obj):
fb.writelines('statements: %s %s\n' % (k, v))
"""
if len(fields_to_update) == 0:
self.module.exit_json(changed=False, lease=response_obj)
else:
msg = OmapiMessage.update(host_response.handle)
msg.update_object(fields_to_update)
try:
response = self.omapi.query_server(msg)
if response.opcode != OMAPI_OP_STATUS:
self.module.fail_json(msg="Failed to modify host, ensure authentication and host parameters "
"are valid.")
self.module.exit_json(changed=True)
except OmapiError as e:
self.module.fail_json(msg="OMAPI error: %s" % to_native(e))
def remove_host(self):
try:
self.omapi.del_host(self.module.params['macaddr'])
self.module.exit_json(changed=True)
except OmapiErrorNotFound:
self.module.exit_json()
except OmapiError as e:
self.module.fail_json(msg="OMAPI error: %s" % to_native(e))
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', required=True, choices=['absent', 'present']),
host=dict(type='str', default="localhost"),
port=dict(type='int', default=7911),
key_name=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=True),
macaddr=dict(type='str', required=True),
hostname=dict(type='str', aliases=['name']),
ip=dict(type='str'),
ddns=dict(type='bool', default=False),
statements=dict(type='list', default=[]),
),
supports_check_mode=False,
)
if not pureomapi_found:
module.fail_json(msg=missing_required_lib('pypureomapi'), exception=PUREOMAPI_IMP_ERR)
if module.params['key'] is None or len(module.params["key"]) == 0:
module.fail_json(msg="'key' parameter cannot be empty.")
if module.params['key_name'] is None or len(module.params["key_name"]) == 0:
module.fail_json(msg="'key_name' parameter cannot be empty.")
host_manager = OmapiHostManager(module)
try:
if module.params['state'] == 'present':
host_manager.setup_host()
elif module.params['state'] == 'absent':
host_manager.remove_host()
except ValueError as e:
module.fail_json(msg="OMAPI input value error: %s" % to_native(e))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,464 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of Networklore's snmp library for Ansible
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: snmp_facts
author:
- Patrick Ogenstad (@ogenstad)
short_description: Retrieve facts for a device using SNMP
description:
- Retrieve facts for a device using SNMP, the facts will be
inserted to the ansible_facts key.
requirements:
- pysnmp
options:
host:
description:
- Set to target snmp server (normally C({{ inventory_hostname }})).
type: str
required: true
version:
description:
- SNMP Version to use, v2/v2c or v3.
type: str
required: true
choices: [ v2, v2c, v3 ]
community:
description:
- The SNMP community string, required if version is v2/v2c.
type: str
level:
description:
- Authentication level.
- Required if version is v3.
type: str
choices: [ authNoPriv, authPriv ]
username:
description:
- Username for SNMPv3.
- Required if version is v3.
type: str
integrity:
description:
- Hashing algorithm.
- Required if version is v3.
type: str
choices: [ md5, sha ]
authkey:
description:
- Authentication key.
- Required if version is v3.
type: str
privacy:
description:
- Encryption algorithm.
- Required if level is authPriv.
type: str
choices: [ aes, des ]
privkey:
description:
- Encryption key.
- Required if version is authPriv.
type: str
'''
EXAMPLES = r'''
- name: Gather facts with SNMP version 2
snmp_facts:
host: '{{ inventory_hostname }}'
version: v2c
community: public
delegate_to: local
- name: Gather facts using SNMP version 3
snmp_facts:
host: '{{ inventory_hostname }}'
version: v3
level: authPriv
integrity: sha
privacy: aes
username: snmp-user
authkey: abc12345
privkey: def6789
delegate_to: localhost
'''
RETURN = r'''
ansible_sysdescr:
description: A textual description of the entity.
returned: success
type: str
sample: Linux ubuntu-user 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64
ansible_sysobjectid:
description: The vendor's authoritative identification of the network management subsystem contained in the entity.
returned: success
type: str
sample: 1.3.6.1.4.1.8072.3.2.10
ansible_sysuptime:
description: The time (in hundredths of a second) since the network management portion of the system was last re-initialized.
returned: success
type: int
sample: 42388
ansible_syscontact:
description: The textual identification of the contact person for this managed node, together with information on how to contact this person.
returned: success
type: str
sample: Me <me@example.org>
ansible_sysname:
description: An administratively-assigned name for this managed node.
returned: success
type: str
sample: ubuntu-user
ansible_syslocation:
description: The physical location of this node (e.g., `telephone closet, 3rd floor').
returned: success
type: str
sample: Sitting on the Dock of the Bay
ansible_all_ipv4_addresses:
description: List of all IPv4 addresses.
returned: success
type: list
sample: ["127.0.0.1", "172.17.0.1"]
ansible_interfaces:
description: Dictionary of each network interface and its metadata.
returned: success
type: dict
sample: {
"1": {
"adminstatus": "up",
"description": "",
"ifindex": "1",
"ipv4": [
{
"address": "127.0.0.1",
"netmask": "255.0.0.0"
}
],
"mac": "",
"mtu": "65536",
"name": "lo",
"operstatus": "up",
"speed": "65536"
},
"2": {
"adminstatus": "up",
"description": "",
"ifindex": "2",
"ipv4": [
{
"address": "192.168.213.128",
"netmask": "255.255.255.0"
}
],
"mac": "000a305a52a1",
"mtu": "1500",
"name": "Intel Corporation 82545EM Gigabit Ethernet Controller (Copper)",
"operstatus": "up",
"speed": "1500"
}
}
'''
import binascii
import traceback
from collections import defaultdict
PYSNMP_IMP_ERR = None
try:
from pysnmp.entity.rfc3413.oneliner import cmdgen
has_pysnmp = True
except Exception:
PYSNMP_IMP_ERR = traceback.format_exc()
has_pysnmp = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_text
class DefineOid(object):
def __init__(self, dotprefix=False):
if dotprefix:
dp = "."
else:
dp = ""
# From SNMPv2-MIB
self.sysDescr = dp + "1.3.6.1.2.1.1.1.0"
self.sysObjectId = dp + "1.3.6.1.2.1.1.2.0"
self.sysUpTime = dp + "1.3.6.1.2.1.1.3.0"
self.sysContact = dp + "1.3.6.1.2.1.1.4.0"
self.sysName = dp + "1.3.6.1.2.1.1.5.0"
self.sysLocation = dp + "1.3.6.1.2.1.1.6.0"
# From IF-MIB
self.ifIndex = dp + "1.3.6.1.2.1.2.2.1.1"
self.ifDescr = dp + "1.3.6.1.2.1.2.2.1.2"
self.ifMtu = dp + "1.3.6.1.2.1.2.2.1.4"
self.ifSpeed = dp + "1.3.6.1.2.1.2.2.1.5"
self.ifPhysAddress = dp + "1.3.6.1.2.1.2.2.1.6"
self.ifAdminStatus = dp + "1.3.6.1.2.1.2.2.1.7"
self.ifOperStatus = dp + "1.3.6.1.2.1.2.2.1.8"
self.ifAlias = dp + "1.3.6.1.2.1.31.1.1.1.18"
# From IP-MIB
self.ipAdEntAddr = dp + "1.3.6.1.2.1.4.20.1.1"
self.ipAdEntIfIndex = dp + "1.3.6.1.2.1.4.20.1.2"
self.ipAdEntNetMask = dp + "1.3.6.1.2.1.4.20.1.3"
def decode_hex(hexstring):
if len(hexstring) < 3:
return hexstring
if hexstring[:2] == "0x":
return to_text(binascii.unhexlify(hexstring[2:]))
else:
return hexstring
def decode_mac(hexstring):
if len(hexstring) != 14:
return hexstring
if hexstring[:2] == "0x":
return hexstring[2:]
else:
return hexstring
def lookup_adminstatus(int_adminstatus):
adminstatus_options = {
1: 'up',
2: 'down',
3: 'testing'
}
if int_adminstatus in adminstatus_options:
return adminstatus_options[int_adminstatus]
else:
return ""
def lookup_operstatus(int_operstatus):
operstatus_options = {
1: 'up',
2: 'down',
3: 'testing',
4: 'unknown',
5: 'dormant',
6: 'notPresent',
7: 'lowerLayerDown'
}
if int_operstatus in operstatus_options:
return operstatus_options[int_operstatus]
else:
return ""
def main():
module = AnsibleModule(
argument_spec=dict(
host=dict(type='str', required=True),
version=dict(type='str', required=True, choices=['v2', 'v2c', 'v3']),
community=dict(type='str'),
username=dict(type='str'),
level=dict(type='str', choices=['authNoPriv', 'authPriv']),
integrity=dict(type='str', choices=['md5', 'sha']),
privacy=dict(type='str', choices=['aes', 'des']),
authkey=dict(type='str'),
privkey=dict(type='str'),
),
required_together=(
['username', 'level', 'integrity', 'authkey'],
['privacy', 'privkey'],
),
supports_check_mode=False,
)
m_args = module.params
if not has_pysnmp:
module.fail_json(msg=missing_required_lib('pysnmp'), exception=PYSNMP_IMP_ERR)
cmdGen = cmdgen.CommandGenerator()
# Verify that we receive a community when using snmp v2
if m_args['version'] == "v2" or m_args['version'] == "v2c":
if m_args['community'] is None:
module.fail_json(msg='Community not set when using snmp version 2')
if m_args['version'] == "v3":
if m_args['username'] is None:
module.fail_json(msg='Username not set when using snmp version 3')
if m_args['level'] == "authPriv" and m_args['privacy'] is None:
module.fail_json(msg='Privacy algorithm not set when using authPriv')
if m_args['integrity'] == "sha":
integrity_proto = cmdgen.usmHMACSHAAuthProtocol
elif m_args['integrity'] == "md5":
integrity_proto = cmdgen.usmHMACMD5AuthProtocol
if m_args['privacy'] == "aes":
privacy_proto = cmdgen.usmAesCfb128Protocol
elif m_args['privacy'] == "des":
privacy_proto = cmdgen.usmDESPrivProtocol
# Use SNMP Version 2
if m_args['version'] == "v2" or m_args['version'] == "v2c":
snmp_auth = cmdgen.CommunityData(m_args['community'])
# Use SNMP Version 3 with authNoPriv
elif m_args['level'] == "authNoPriv":
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], authProtocol=integrity_proto)
# Use SNMP Version 3 with authPriv
else:
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], privKey=m_args['privkey'], authProtocol=integrity_proto,
privProtocol=privacy_proto)
# Use p to prefix OIDs with a dot for polling
p = DefineOid(dotprefix=True)
# Use v without a prefix to use with return values
v = DefineOid(dotprefix=False)
def Tree():
return defaultdict(Tree)
results = Tree()
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args['host'], 161)),
cmdgen.MibVariable(p.sysDescr,),
cmdgen.MibVariable(p.sysObjectId,),
cmdgen.MibVariable(p.sysUpTime,),
cmdgen.MibVariable(p.sysContact,),
cmdgen.MibVariable(p.sysName,),
cmdgen.MibVariable(p.sysLocation,),
lookupMib=False
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
for oid, val in varBinds:
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if current_oid == v.sysDescr:
results['ansible_sysdescr'] = decode_hex(current_val)
elif current_oid == v.sysObjectId:
results['ansible_sysobjectid'] = current_val
elif current_oid == v.sysUpTime:
results['ansible_sysuptime'] = current_val
elif current_oid == v.sysContact:
results['ansible_syscontact'] = current_val
elif current_oid == v.sysName:
results['ansible_sysname'] = current_val
elif current_oid == v.sysLocation:
results['ansible_syslocation'] = current_val
errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args['host'], 161)),
cmdgen.MibVariable(p.ifIndex,),
cmdgen.MibVariable(p.ifDescr,),
cmdgen.MibVariable(p.ifMtu,),
cmdgen.MibVariable(p.ifSpeed,),
cmdgen.MibVariable(p.ifPhysAddress,),
cmdgen.MibVariable(p.ifAdminStatus,),
cmdgen.MibVariable(p.ifOperStatus,),
cmdgen.MibVariable(p.ipAdEntAddr,),
cmdgen.MibVariable(p.ipAdEntIfIndex,),
cmdgen.MibVariable(p.ipAdEntNetMask,),
cmdgen.MibVariable(p.ifAlias,),
lookupMib=False
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
interface_indexes = []
all_ipv4_addresses = []
ipv4_networks = Tree()
for varBinds in varTable:
for oid, val in varBinds:
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if v.ifIndex in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['ifindex'] = current_val
interface_indexes.append(ifIndex)
if v.ifDescr in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['name'] = current_val
if v.ifMtu in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['mtu'] = current_val
if v.ifSpeed in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['speed'] = current_val
if v.ifPhysAddress in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['mac'] = decode_mac(current_val)
if v.ifAdminStatus in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['adminstatus'] = lookup_adminstatus(int(current_val))
if v.ifOperStatus in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['operstatus'] = lookup_operstatus(int(current_val))
if v.ipAdEntAddr in current_oid:
curIPList = current_oid.rsplit('.', 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]['address'] = current_val
all_ipv4_addresses.append(current_val)
if v.ipAdEntIfIndex in current_oid:
curIPList = current_oid.rsplit('.', 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]['interface'] = current_val
if v.ipAdEntNetMask in current_oid:
curIPList = current_oid.rsplit('.', 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]['netmask'] = current_val
if v.ifAlias in current_oid:
ifIndex = int(current_oid.rsplit('.', 1)[-1])
results['ansible_interfaces'][ifIndex]['description'] = current_val
interface_to_ipv4 = {}
for ipv4_network in ipv4_networks:
current_interface = ipv4_networks[ipv4_network]['interface']
current_network = {
'address': ipv4_networks[ipv4_network]['address'],
'netmask': ipv4_networks[ipv4_network]['netmask']
}
if current_interface not in interface_to_ipv4:
interface_to_ipv4[current_interface] = []
interface_to_ipv4[current_interface].append(current_network)
else:
interface_to_ipv4[current_interface].append(current_network)
for interface in interface_to_ipv4:
results['ansible_interfaces'][int(interface)]['ipv4'] = interface_to_ipv4[interface]
results['ansible_all_ipv4_addresses'] = all_ipv4_addresses
module.exit_json(ansible_facts=results)
if __name__ == '__main__':
main()