mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 12:50:22 -07:00
Split modules/network into two parts (#24024)
* Split modules/network into two parts Given the dedicated team we we have working on Ansible Networking a clearer split is needed between Networking modules and "things that happen to use the network" * nmcli to net_tools * nmcli moved
This commit is contained in:
parent
2d9d1762ba
commit
1c61b9bae7
23 changed files with 12 additions and 12 deletions
|
@ -1,489 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# see examples/playbooks/get_url.yml
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: get_url
|
||||
short_description: Downloads files from HTTP, HTTPS, or FTP to node
|
||||
description:
|
||||
- Downloads files from HTTP, HTTPS, or FTP to the remote server. The remote
|
||||
server I(must) have direct access to the remote resource.
|
||||
- By default, if an environment variable C(<protocol>_proxy) is set on
|
||||
the target host, requests will be sent through that proxy. This
|
||||
behaviour can be overridden by setting a variable for this task
|
||||
(see `setting the environment
|
||||
<http://docs.ansible.com/playbooks_environment.html>`_),
|
||||
or by using the use_proxy option.
|
||||
- HTTP redirects can redirect from HTTP to HTTPS so you should be sure that
|
||||
your proxy environment for both protocols is correct.
|
||||
version_added: "0.6"
|
||||
options:
|
||||
url:
|
||||
description:
|
||||
- HTTP, HTTPS, or FTP URL in the form (http|https|ftp)://[user[:pass]]@host.domain[:port]/path
|
||||
required: true
|
||||
dest:
|
||||
description:
|
||||
- absolute path of where to download the file to.
|
||||
- If C(dest) is a directory, either the server provided filename or, if
|
||||
none provided, the base name of the URL on the remote server will be
|
||||
used. If a directory, C(force) has no effect.
|
||||
If C(dest) is a directory, the file will always be
|
||||
downloaded (regardless of the force option), but replaced only if the contents changed.
|
||||
required: true
|
||||
tmp_dest:
|
||||
description:
|
||||
- absolute path of where temporary file is downloaded to.
|
||||
- Defaults to TMPDIR, TEMP or TMP env variables or a platform specific value
|
||||
- https://docs.python.org/2/library/tempfile.html#tempfile.tempdir
|
||||
required: false
|
||||
default: ''
|
||||
version_added: '2.1'
|
||||
force:
|
||||
description:
|
||||
- If C(yes) and C(dest) is not a directory, will download the file every
|
||||
time and replace the file if the contents change. If C(no), the file
|
||||
will only be downloaded if the destination does not exist. Generally
|
||||
should be C(yes) only for small local files. Prior to 0.6, this module
|
||||
behaved as if C(yes) was the default.
|
||||
version_added: "0.7"
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
aliases: [ "thirsty" ]
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can get
|
||||
the original file back if you somehow clobbered it incorrectly.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
version_added: '2.1'
|
||||
sha256sum:
|
||||
description:
|
||||
- If a SHA-256 checksum is passed to this parameter, the digest of the
|
||||
destination file will be calculated after it is downloaded to ensure
|
||||
its integrity and verify that the transfer completed successfully.
|
||||
This option is deprecated. Use 'checksum'.
|
||||
version_added: "1.3"
|
||||
required: false
|
||||
default: null
|
||||
checksum:
|
||||
description:
|
||||
- 'If a checksum is passed to this parameter, the digest of the
|
||||
destination file will be calculated after it is downloaded to ensure
|
||||
its integrity and verify that the transfer completed successfully.
|
||||
Format: <algorithm>:<checksum>, e.g.: checksum="sha256:D98291AC[...]B6DC7B97"
|
||||
If you worry about portability, only the sha1 algorithm is available
|
||||
on all platforms and python versions. The third party hashlib
|
||||
library can be installed for access to additional algorithms.
|
||||
Additionally, if a checksum is passed to this parameter, and the file exist under
|
||||
the C(dest) location, the destination_checksum would be calculated, and if
|
||||
checksum equals destination_checksum, the file download would be skipped
|
||||
(unless C(force) is true). '
|
||||
version_added: "2.0"
|
||||
required: false
|
||||
default: null
|
||||
use_proxy:
|
||||
description:
|
||||
- if C(no), it will not use a proxy, even if one is defined in
|
||||
an environment variable on the target hosts.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
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.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
timeout:
|
||||
description:
|
||||
- Timeout in seconds for URL request
|
||||
required: false
|
||||
default: 10
|
||||
version_added: '1.8'
|
||||
headers:
|
||||
description:
|
||||
- 'Add custom HTTP headers to a request in the format "key:value,key:value"'
|
||||
required: false
|
||||
default: null
|
||||
version_added: '2.0'
|
||||
url_username:
|
||||
description:
|
||||
- The username for use in HTTP basic authentication. This parameter can be used
|
||||
without C(url_password) for sites that allow empty passwords.
|
||||
required: false
|
||||
version_added: '1.6'
|
||||
url_password:
|
||||
description:
|
||||
- The password for use in HTTP basic authentication. If the C(url_username)
|
||||
parameter is not specified, the C(url_password) parameter will not be used.
|
||||
required: false
|
||||
version_added: '1.6'
|
||||
force_basic_auth:
|
||||
version_added: '2.0'
|
||||
description:
|
||||
- httplib2, the library used by the uri module only sends authentication information when a webservice
|
||||
responds to an initial request with a 401 status. Since some basic auth services do not properly
|
||||
send a 401, logins will fail. This option forces the sending of the Basic authentication header
|
||||
upon initial request.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
client_cert:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- PEM formatted certificate chain file to be used for SSL client
|
||||
authentication. This file can also include the key as well, and if
|
||||
the key is included, I(client_key) is not required
|
||||
version_added: 2.4
|
||||
client_key:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- PEM formatted file that contains your private key to be used for SSL
|
||||
client authentication. If I(client_cert) contains both the certificate
|
||||
and key, this option is not required.
|
||||
version_added: 2.4
|
||||
others:
|
||||
description:
|
||||
- all arguments accepted by the M(file) module also work here
|
||||
required: false
|
||||
# informational: requirements for nodes
|
||||
requirements: [ ]
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
author: "Jan-Piet Mens (@jpmens)"
|
||||
'''
|
||||
|
||||
EXAMPLES='''
|
||||
- name: download foo.conf
|
||||
get_url:
|
||||
url: http://example.com/path/file.conf
|
||||
dest: /etc/foo.conf
|
||||
mode: 0440
|
||||
|
||||
- name: download file and force basic auth
|
||||
get_url:
|
||||
url: http://example.com/path/file.conf
|
||||
dest: /etc/foo.conf
|
||||
force_basic_auth: yes
|
||||
|
||||
- name: download file with custom HTTP headers
|
||||
get_url:
|
||||
url: http://example.com/path/file.conf
|
||||
dest: /etc/foo.conf
|
||||
headers: 'key:value,key:value'
|
||||
|
||||
- name: download file with check (sha256)
|
||||
get_url:
|
||||
url: http://example.com/path/file.conf
|
||||
dest: /etc/foo.conf
|
||||
checksum: sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
|
||||
|
||||
- name: download file with check (md5)
|
||||
get_url:
|
||||
url: http://example.com/path/file.conf
|
||||
dest: /etc/foo.conf
|
||||
checksum: md5:66dffb5228a211e61d6d7ef4a86f5758
|
||||
|
||||
- name: download file from a file path
|
||||
get_url:
|
||||
url: "file:///tmp/afile.txt"
|
||||
dest: /tmp/afilecopy.txt
|
||||
'''
|
||||
|
||||
import shutil
|
||||
import datetime
|
||||
import re
|
||||
import tempfile
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlsplit
|
||||
|
||||
# ==============================================================
|
||||
# url handling
|
||||
|
||||
def url_filename(url):
|
||||
fn = os.path.basename(urlsplit(url)[2])
|
||||
if fn == '':
|
||||
return 'index.html'
|
||||
return fn
|
||||
|
||||
def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, headers=None, tmp_dest=''):
|
||||
"""
|
||||
Download data from the url and store in a temporary file.
|
||||
|
||||
Return (tempfile, info about the request)
|
||||
"""
|
||||
|
||||
rsp, info = fetch_url(module, url, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, headers=headers)
|
||||
|
||||
if info['status'] == 304:
|
||||
module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', ''))
|
||||
|
||||
# Exceptions in fetch_url may result in a status -1, the ensures a proper error to the user in all cases
|
||||
if info['status'] == -1:
|
||||
module.fail_json(msg=info['msg'], url=url, dest=dest)
|
||||
|
||||
if info['status'] != 200 and not url.startswith('file:/') and not (url.startswith('ftp:/') and info.get('msg', '').startswith('OK')):
|
||||
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url, dest=dest)
|
||||
|
||||
# create a temporary file and copy content to do checksum-based replacement
|
||||
if tmp_dest != '':
|
||||
# tmp_dest should be an existing dir
|
||||
tmp_dest_is_dir = os.path.isdir(tmp_dest)
|
||||
if not tmp_dest_is_dir:
|
||||
if os.path.exists(tmp_dest):
|
||||
module.fail_json(msg="%s is a file but should be a directory." % tmp_dest)
|
||||
else:
|
||||
module.fail_json(msg="%s directory does not exist." % tmp_dest)
|
||||
|
||||
fd, tempname = tempfile.mkstemp(dir=tmp_dest)
|
||||
else:
|
||||
fd, tempname = tempfile.mkstemp()
|
||||
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
shutil.copyfileobj(rsp, f)
|
||||
except Exception:
|
||||
err = get_exception()
|
||||
os.remove(tempname)
|
||||
module.fail_json(msg="failed to create temporary content file: %s" % str(err))
|
||||
f.close()
|
||||
rsp.close()
|
||||
return tempname, info
|
||||
|
||||
def extract_filename_from_headers(headers):
|
||||
"""
|
||||
Extracts a filename from the given dict of HTTP headers.
|
||||
|
||||
Looks for the content-disposition header and applies a regex.
|
||||
Returns the filename if successful, else None."""
|
||||
cont_disp_regex = 'attachment; ?filename="?([^"]+)'
|
||||
res = None
|
||||
|
||||
if 'content-disposition' in headers:
|
||||
cont_disp = headers['content-disposition']
|
||||
match = re.match(cont_disp_regex, cont_disp)
|
||||
if match:
|
||||
res = match.group(1)
|
||||
# Try preventing any funny business.
|
||||
res = os.path.basename(res)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
argument_spec = url_argument_spec()
|
||||
argument_spec.update(
|
||||
url = dict(required=True),
|
||||
dest = dict(required=True, type='path'),
|
||||
backup = dict(default=False, type='bool'),
|
||||
sha256sum = dict(default=''),
|
||||
checksum = dict(default=''),
|
||||
timeout = dict(required=False, type='int', default=10),
|
||||
headers = dict(required=False, default=None),
|
||||
tmp_dest = dict(required=False, default='', type='path'),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
argument_spec = argument_spec,
|
||||
add_file_common_args=True
|
||||
)
|
||||
|
||||
url = module.params['url']
|
||||
dest = module.params['dest']
|
||||
backup = module.params['backup']
|
||||
force = module.params['force']
|
||||
sha256sum = module.params['sha256sum']
|
||||
checksum = module.params['checksum']
|
||||
use_proxy = module.params['use_proxy']
|
||||
timeout = module.params['timeout']
|
||||
tmp_dest = module.params['tmp_dest']
|
||||
|
||||
# Parse headers to dict
|
||||
if module.params['headers']:
|
||||
try:
|
||||
headers = dict(item.split(':', 1) for item in module.params['headers'].split(','))
|
||||
except:
|
||||
module.fail_json(msg="The header parameter requires a key:value,key:value syntax to be properly parsed.")
|
||||
else:
|
||||
headers = None
|
||||
|
||||
dest_is_dir = os.path.isdir(dest)
|
||||
last_mod_time = None
|
||||
|
||||
# workaround for usage of deprecated sha256sum parameter
|
||||
if sha256sum != '':
|
||||
checksum = 'sha256:%s' % (sha256sum)
|
||||
|
||||
# checksum specified, parse for algorithm and checksum
|
||||
if checksum != '':
|
||||
try:
|
||||
algorithm, checksum = checksum.rsplit(':', 1)
|
||||
# Remove any non-alphanumeric characters, including the infamous
|
||||
# Unicode zero-width space
|
||||
checksum = re.sub(r'\W+', '', checksum).lower()
|
||||
# Ensure the checksum portion is a hexdigest
|
||||
int(checksum, 16)
|
||||
except ValueError:
|
||||
module.fail_json(msg="The checksum parameter has to be in format <algorithm>:<checksum>")
|
||||
|
||||
if not dest_is_dir and os.path.exists(dest):
|
||||
checksum_mismatch = False
|
||||
|
||||
# If the download is not forced and there is a checksum, allow
|
||||
# checksum match to skip the download.
|
||||
if not force and checksum != '':
|
||||
destination_checksum = module.digest_from_file(dest, algorithm)
|
||||
|
||||
if checksum == destination_checksum:
|
||||
module.exit_json(msg="file already exists", dest=dest, url=url, changed=False)
|
||||
|
||||
checksum_mismatch = True
|
||||
|
||||
# Not forcing redownload, unless checksum does not match
|
||||
if not force and not checksum_mismatch:
|
||||
# allow file attribute changes
|
||||
module.params['path'] = dest
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
file_args['path'] = dest
|
||||
changed = module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
if changed:
|
||||
module.exit_json(msg="file already exists but file attributes changed", dest=dest, url=url, changed=changed)
|
||||
module.exit_json(msg="file already exists", dest=dest, url=url, changed=changed)
|
||||
|
||||
# If the file already exists, prepare the last modified time for the
|
||||
# request.
|
||||
mtime = os.path.getmtime(dest)
|
||||
last_mod_time = datetime.datetime.utcfromtimestamp(mtime)
|
||||
|
||||
# If the checksum does not match we have to force the download
|
||||
# because last_mod_time may be newer than on remote
|
||||
if checksum_mismatch:
|
||||
force = True
|
||||
|
||||
# download to tmpsrc
|
||||
tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout, headers, tmp_dest)
|
||||
|
||||
# Now the request has completed, we can finally generate the final
|
||||
# destination file name from the info dict.
|
||||
|
||||
if dest_is_dir:
|
||||
filename = extract_filename_from_headers(info)
|
||||
if not filename:
|
||||
# Fall back to extracting the filename from the URL.
|
||||
# Pluck the URL from the info, since a redirect could have changed
|
||||
# it.
|
||||
filename = url_filename(info['url'])
|
||||
dest = os.path.join(dest, filename)
|
||||
|
||||
checksum_src = None
|
||||
checksum_dest = None
|
||||
|
||||
# raise an error if there is no tmpsrc file
|
||||
if not os.path.exists(tmpsrc):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'])
|
||||
if not os.access(tmpsrc, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Source %s not readable" % (tmpsrc))
|
||||
checksum_src = module.sha1(tmpsrc)
|
||||
|
||||
# check if there is no dest file
|
||||
if os.path.exists(dest):
|
||||
# raise an error if copy has no permission on dest
|
||||
if not os.access(dest, os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not readable" % (dest))
|
||||
checksum_dest = module.sha1(dest)
|
||||
else:
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not writable" % (os.path.dirname(dest)))
|
||||
|
||||
backup_file = None
|
||||
if checksum_src != checksum_dest:
|
||||
try:
|
||||
if backup:
|
||||
if os.path.exists(dest):
|
||||
backup_file = module.backup_local(dest)
|
||||
shutil.copyfile(tmpsrc, dest)
|
||||
except Exception:
|
||||
err = get_exception()
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err)))
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
|
||||
if checksum != '':
|
||||
destination_checksum = module.digest_from_file(dest, algorithm)
|
||||
|
||||
if checksum != destination_checksum:
|
||||
os.remove(dest)
|
||||
module.fail_json(msg="The checksum for %s did not match %s; it was %s." % (dest, checksum, destination_checksum))
|
||||
|
||||
os.remove(tmpsrc)
|
||||
|
||||
# allow file attribute changes
|
||||
module.params['path'] = dest
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
file_args['path'] = dest
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
|
||||
# Backwards compat only. We'll return None on FIPS enabled systems
|
||||
try:
|
||||
md5sum = module.md5(dest)
|
||||
except ValueError:
|
||||
md5sum = None
|
||||
|
||||
res_args = dict(
|
||||
url = url, dest = dest, src = tmpsrc, md5sum = md5sum, checksum_src = checksum_src,
|
||||
checksum_dest = checksum_dest, changed = changed, msg = info.get('msg', ''), status_code=info.get('status','')
|
||||
)
|
||||
if backup_file:
|
||||
res_args['backup_file'] = backup_file
|
||||
|
||||
# Mission complete
|
||||
module.exit_json(**res_args)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: slurp
|
||||
version_added: historical
|
||||
short_description: Slurps a file from remote nodes
|
||||
description:
|
||||
- This module works like M(fetch). It is used for fetching a base64-
|
||||
encoded blob containing the data in a remote file.
|
||||
options:
|
||||
src:
|
||||
description:
|
||||
- The file on the remote system to fetch. This I(must) be a file, not a
|
||||
directory.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
notes:
|
||||
- This module returns an 'in memory' base64 encoded version of the file, take into account that this will require at least twice the RAM as the
|
||||
original file size.
|
||||
- "See also: M(fetch)"
|
||||
requirements: []
|
||||
author:
|
||||
- "Ansible Core Team"
|
||||
- "Michael DeHaan"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Find out what the remote machine's mounts are:
|
||||
- slurp:
|
||||
src: /proc/mounts
|
||||
register: mounts
|
||||
|
||||
- debug:
|
||||
msg: "{{ mounts['content'] | b64decode }}"
|
||||
|
||||
# From the commandline, find the pid of the remote machine's sshd
|
||||
# $ ansible host -m slurp -a 'src=/var/run/sshd.pid'
|
||||
# host | SUCCESS => {
|
||||
# "changed": false,
|
||||
# "content": "MjE3OQo=",
|
||||
# "encoding": "base64",
|
||||
# "source": "/var/run/sshd.pid"
|
||||
# }
|
||||
# $ echo MjE3OQo= | base64 -d
|
||||
# 2179
|
||||
'''
|
||||
|
||||
import base64
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
src = dict(required=True, aliases=['path'], type='path'),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
source = module.params['src']
|
||||
|
||||
if not os.path.exists(source):
|
||||
module.fail_json(msg="file not found: %s" % source)
|
||||
if not os.access(source, os.R_OK):
|
||||
module.fail_json(msg="file is not readable: %s" % source)
|
||||
|
||||
data = base64.b64encode(open(source, 'rb').read())
|
||||
|
||||
module.exit_json(content=data, source=source, encoding='base64')
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -1,514 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Romeo Theriault <romeot () hawaii.edu>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# see examples/playbooks/uri.yml
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: uri
|
||||
short_description: Interacts with webservices
|
||||
description:
|
||||
- Interacts with HTTP and HTTPS web services and supports Digest, Basic and WSSE
|
||||
HTTP authentication mechanisms.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
url:
|
||||
description:
|
||||
- HTTP or HTTPS URL in the form (http|https)://host.domain[:port]/path
|
||||
required: true
|
||||
default: null
|
||||
dest:
|
||||
description:
|
||||
- path of where to download the file to (if desired). If I(dest) is a
|
||||
directory, the basename of the file on the remote server will be used.
|
||||
required: false
|
||||
default: null
|
||||
user:
|
||||
description:
|
||||
- username for the module to use for Digest, Basic or WSSE authentication.
|
||||
required: false
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- password for the module to use for Digest, Basic or WSSE authentication.
|
||||
required: false
|
||||
default: null
|
||||
body:
|
||||
description:
|
||||
- The body of the http request/response to the web service. If C(body_format) is set
|
||||
to 'json' it will take an already formatted JSON string or convert a data structure
|
||||
into JSON.
|
||||
required: false
|
||||
default: null
|
||||
body_format:
|
||||
description:
|
||||
- The serialization format of the body. When set to json, encodes the
|
||||
body argument, if needed, and automatically sets the Content-Type header accordingly.
|
||||
As of C(2.3) it is possible to override the `Content-Type` header, when
|
||||
set to json via the I(headers) option.
|
||||
required: false
|
||||
choices: [ "raw", "json" ]
|
||||
default: raw
|
||||
version_added: "2.0"
|
||||
method:
|
||||
description:
|
||||
- The HTTP method of the request or response. It MUST be uppercase.
|
||||
required: false
|
||||
choices: [ "GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "PATCH", "TRACE", "CONNECT", "REFRESH" ]
|
||||
default: "GET"
|
||||
return_content:
|
||||
description:
|
||||
- Whether or not to return the body of the request as a "content" key in
|
||||
the dictionary result. If the reported Content-type is
|
||||
"application/json", then the JSON is additionally loaded into a key
|
||||
called C(json) in the dictionary results.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
force_basic_auth:
|
||||
description:
|
||||
- The library used by the uri module only sends authentication information when a webservice
|
||||
responds to an initial request with a 401 status. Since some basic auth services do not properly
|
||||
send a 401, logins will fail. This option forces the sending of the Basic authentication header
|
||||
upon initial request.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
follow_redirects:
|
||||
description:
|
||||
- Whether or not the URI module should follow redirects. C(all) will follow all redirects.
|
||||
C(safe) will follow only "safe" redirects, where "safe" means that the client is only
|
||||
doing a GET or HEAD on the URI to which it is being redirected. C(none) will not follow
|
||||
any redirects. Note that C(yes) and C(no) choices are accepted for backwards compatibility,
|
||||
where C(yes) is the equivalent of C(all) and C(no) is the equivalent of C(safe). C(yes) and C(no)
|
||||
are deprecated and will be removed in some future version of Ansible.
|
||||
required: false
|
||||
choices: [ "all", "safe", "none" ]
|
||||
default: "safe"
|
||||
creates:
|
||||
description:
|
||||
- a filename, when it already exists, this step will not be run.
|
||||
required: false
|
||||
removes:
|
||||
description:
|
||||
- a filename, when it does not exist, this step will not be run.
|
||||
required: false
|
||||
status_code:
|
||||
description:
|
||||
- A valid, numeric, HTTP status code that signifies success of the
|
||||
request. Can also be comma separated list of status codes.
|
||||
required: false
|
||||
default: 200
|
||||
timeout:
|
||||
description:
|
||||
- The socket level timeout in seconds
|
||||
required: false
|
||||
default: 30
|
||||
HEADER_:
|
||||
description:
|
||||
- Any parameter starting with "HEADER_" is a sent with your request as a header.
|
||||
For example, HEADER_Content-Type="application/json" would send the header
|
||||
"Content-Type" along with your request with a value of "application/json".
|
||||
This option is deprecated as of C(2.1) and may be removed in a future
|
||||
release. Use I(headers) instead.
|
||||
required: false
|
||||
default: null
|
||||
headers:
|
||||
description:
|
||||
- Add custom HTTP headers to a request in the format of a YAML hash. As
|
||||
of C(2.3) supplying C(Content-Type) here will override the header
|
||||
generated by supplying C(json) for I(body_format).
|
||||
required: false
|
||||
default: null
|
||||
version_added: '2.1'
|
||||
others:
|
||||
description:
|
||||
- all arguments accepted by the M(file) module also work here
|
||||
required: false
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only
|
||||
set to C(no) used on personally controlled sites using self-signed
|
||||
certificates. Prior to 1.9.2 the code defaulted to C(no).
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: '1.9.2'
|
||||
client_cert:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- PEM formatted certificate chain file to be used for SSL client
|
||||
authentication. This file can also include the key as well, and if
|
||||
the key is included, I(client_key) is not required
|
||||
version_added: 2.4
|
||||
client_key:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- PEM formatted file that contains your private key to be used for SSL
|
||||
client authentication. If I(client_cert) contains both the certificate
|
||||
and key, this option is not required.
|
||||
version_added: 2.4
|
||||
notes:
|
||||
- The dependency on httplib2 was removed in Ansible 2.1
|
||||
author: "Romeo Theriault (@romeotheriault)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Check that you can connect (GET) to a page and it returns a status 200
|
||||
uri:
|
||||
url: http://www.example.com
|
||||
|
||||
# Check that a page returns a status 200 and fail if the word AWESOME is not
|
||||
# in the page contents.
|
||||
- uri:
|
||||
url: http://www.example.com
|
||||
return_content: yes
|
||||
register: webpage
|
||||
|
||||
- name: Fail if AWESOME is not in the page content
|
||||
fail:
|
||||
when: "'AWESOME' not in webpage.content"
|
||||
|
||||
|
||||
- name: Create a JIRA issue
|
||||
uri:
|
||||
url: https://your.jira.example.com/rest/api/2/issue/
|
||||
method: POST
|
||||
user: your_username
|
||||
password: your_pass
|
||||
body: "{{ lookup('file','issue.json') }}"
|
||||
force_basic_auth: yes
|
||||
status_code: 201
|
||||
body_format: json
|
||||
|
||||
# Login to a form based webpage, then use the returned cookie to
|
||||
# access the app in later tasks
|
||||
|
||||
- uri:
|
||||
url: https://your.form.based.auth.example.com/index.php
|
||||
method: POST
|
||||
body: "name=your_username&password=your_password&enter=Sign%20in"
|
||||
status_code: 302
|
||||
headers:
|
||||
Content-Type: "application/x-www-form-urlencoded"
|
||||
register: login
|
||||
|
||||
- uri:
|
||||
url: https://your.form.based.auth.example.com/dashboard.php
|
||||
method: GET
|
||||
return_content: yes
|
||||
headers:
|
||||
Cookie: "{{login.set_cookie}}"
|
||||
|
||||
- name: Queue build of a project in Jenkins
|
||||
uri:
|
||||
url: "http://{{ jenkins.host }}/job/{{ jenkins.job }}/build?token={{ jenkins.token }}"
|
||||
method: GET
|
||||
user: "{{ jenkins.user }}"
|
||||
password: "{{ jenkins.password }}"
|
||||
force_basic_auth: yes
|
||||
status_code: 201
|
||||
|
||||
'''
|
||||
|
||||
import cgi
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
import ansible.module_utils.six as six
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.urls import fetch_url, url_argument_spec
|
||||
|
||||
|
||||
def write_file(module, url, dest, content):
|
||||
# create a tempfile with some test content
|
||||
fd, tmpsrc = tempfile.mkstemp()
|
||||
f = open(tmpsrc, 'wb')
|
||||
try:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
err = get_exception()
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="failed to create temporary content file: %s" % str(err))
|
||||
f.close()
|
||||
|
||||
checksum_src = None
|
||||
checksum_dest = None
|
||||
|
||||
# raise an error if there is no tmpsrc file
|
||||
if not os.path.exists(tmpsrc):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Source %s does not exist" % (tmpsrc))
|
||||
if not os.access(tmpsrc, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Source %s not readable" % (tmpsrc))
|
||||
checksum_src = module.sha1(tmpsrc)
|
||||
|
||||
# check if there is no dest file
|
||||
if os.path.exists(dest):
|
||||
# raise an error if copy has no permission on dest
|
||||
if not os.access(dest, os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Destination %s not readable" % (dest))
|
||||
checksum_dest = module.sha1(dest)
|
||||
else:
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Destination dir %s not writable" % (os.path.dirname(dest)))
|
||||
|
||||
if checksum_src != checksum_dest:
|
||||
try:
|
||||
shutil.copyfile(tmpsrc, dest)
|
||||
except Exception:
|
||||
err = get_exception()
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err)))
|
||||
|
||||
os.remove(tmpsrc)
|
||||
|
||||
|
||||
def url_filename(url):
|
||||
fn = os.path.basename(six.moves.urllib.parse.urlsplit(url)[2])
|
||||
if fn == '':
|
||||
return 'index.html'
|
||||
return fn
|
||||
|
||||
|
||||
def absolute_location(url, location):
|
||||
"""Attempts to create an absolute URL based on initial URL, and
|
||||
next URL, specifically in the case of a ``Location`` header.
|
||||
"""
|
||||
|
||||
if '://' in location:
|
||||
return location
|
||||
|
||||
elif location.startswith('/'):
|
||||
parts = six.moves.urllib.parse.urlsplit(url)
|
||||
base = url.replace(parts[2], '')
|
||||
return '%s%s' % (base, location)
|
||||
|
||||
elif not location.startswith('/'):
|
||||
base = os.path.dirname(url)
|
||||
return '%s/%s' % (base, location)
|
||||
|
||||
else:
|
||||
return location
|
||||
|
||||
|
||||
def uri(module, url, dest, body, body_format, method, headers, socket_timeout):
|
||||
# is dest is set and is a directory, let's check if we get redirected and
|
||||
# set the filename from that url
|
||||
redirected = False
|
||||
redir_info = {}
|
||||
r = {}
|
||||
if dest is not None:
|
||||
# Stash follow_redirects, in this block we don't want to follow
|
||||
# we'll reset back to the supplied value soon
|
||||
follow_redirects = module.params['follow_redirects']
|
||||
module.params['follow_redirects'] = False
|
||||
dest = os.path.expanduser(dest)
|
||||
if os.path.isdir(dest):
|
||||
# first check if we are redirected to a file download
|
||||
_, redir_info = fetch_url(module, url, data=body,
|
||||
headers=headers,
|
||||
method=method,
|
||||
timeout=socket_timeout)
|
||||
# if we are redirected, update the url with the location header,
|
||||
# and update dest with the new url filename
|
||||
if redir_info['status'] in (301, 302, 303, 307):
|
||||
url = redir_info['location']
|
||||
redirected = True
|
||||
dest = os.path.join(dest, url_filename(url))
|
||||
# if destination file already exist, only download if file newer
|
||||
if os.path.exists(dest):
|
||||
t = datetime.datetime.utcfromtimestamp(os.path.getmtime(dest))
|
||||
tstamp = t.strftime('%a, %d %b %Y %H:%M:%S +0000')
|
||||
headers['If-Modified-Since'] = tstamp
|
||||
|
||||
# Reset follow_redirects back to the stashed value
|
||||
module.params['follow_redirects'] = follow_redirects
|
||||
|
||||
resp, info = fetch_url(module, url, data=body, headers=headers,
|
||||
method=method, timeout=socket_timeout)
|
||||
|
||||
try:
|
||||
content = resp.read()
|
||||
except AttributeError:
|
||||
# there was no content, but the error read()
|
||||
# may have been stored in the info as 'body'
|
||||
content = info.pop('body', '')
|
||||
|
||||
r['redirected'] = redirected or info['url'] != url
|
||||
r.update(redir_info)
|
||||
r.update(info)
|
||||
|
||||
return r, content, dest
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = url_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
dest = dict(required=False, default=None, type='path'),
|
||||
url_username = dict(required=False, default=None, aliases=['user']),
|
||||
url_password = dict(required=False, default=None, aliases=['password'], no_log=True),
|
||||
body = dict(required=False, default=None, type='raw'),
|
||||
body_format = dict(required=False, default='raw', choices=['raw', 'json']),
|
||||
method = dict(required=False, default='GET', choices=['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH', 'TRACE', 'CONNECT', 'REFRESH']),
|
||||
return_content = dict(required=False, default='no', type='bool'),
|
||||
follow_redirects = dict(required=False, default='safe', choices=['all', 'safe', 'none', 'yes', 'no']),
|
||||
creates = dict(required=False, default=None, type='path'),
|
||||
removes = dict(required=False, default=None, type='path'),
|
||||
status_code = dict(required=False, default=[200], type='list'),
|
||||
timeout = dict(required=False, default=30, type='int'),
|
||||
headers = dict(required=False, type='dict', default={})
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
check_invalid_arguments=False,
|
||||
add_file_common_args=True
|
||||
)
|
||||
|
||||
url = module.params['url']
|
||||
body = module.params['body']
|
||||
body_format = module.params['body_format'].lower()
|
||||
method = module.params['method']
|
||||
dest = module.params['dest']
|
||||
return_content = module.params['return_content']
|
||||
creates = module.params['creates']
|
||||
removes = module.params['removes']
|
||||
status_code = [int(x) for x in list(module.params['status_code'])]
|
||||
socket_timeout = module.params['timeout']
|
||||
|
||||
dict_headers = module.params['headers']
|
||||
|
||||
if body_format == 'json':
|
||||
# Encode the body unless its a string, then assume it is pre-formatted JSON
|
||||
if not isinstance(body, six.string_types):
|
||||
body = json.dumps(body)
|
||||
lower_header_keys = [key.lower() for key in dict_headers]
|
||||
if 'content-type' not in lower_header_keys:
|
||||
dict_headers['Content-Type'] = 'application/json'
|
||||
|
||||
# Grab all the http headers. Need this hack since passing multi-values is
|
||||
# currently a bit ugly. (e.g. headers='{"Content-Type":"application/json"}')
|
||||
for key, value in six.iteritems(module.params):
|
||||
if key.startswith("HEADER_"):
|
||||
module.deprecate('Supplying headers via HEADER_* is deprecated and '
|
||||
'will be removed in a future version. Please use '
|
||||
'`headers` to supply headers for the request')
|
||||
skey = key.replace("HEADER_", "")
|
||||
dict_headers[skey] = value
|
||||
|
||||
if creates is not None:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of uri executions.
|
||||
if os.path.exists(creates):
|
||||
module.exit_json(stdout="skipped, since %s exists" % creates, changed=False, rc=0)
|
||||
|
||||
if removes is not None:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename do not exists. This allows idempotence
|
||||
# of uri executions.
|
||||
if not os.path.exists(removes):
|
||||
module.exit_json(stdout="skipped, since %s does not exist" % removes, changed=False, rc=0)
|
||||
|
||||
# Make the request
|
||||
resp, content, dest = uri(module, url, dest, body, body_format, method,
|
||||
dict_headers, socket_timeout)
|
||||
resp['status'] = int(resp['status'])
|
||||
|
||||
# Write the file out if requested
|
||||
if dest is not None:
|
||||
if resp['status'] == 304:
|
||||
changed = False
|
||||
else:
|
||||
write_file(module, url, dest, content)
|
||||
# allow file attribute changes
|
||||
changed = True
|
||||
module.params['path'] = dest
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
file_args['path'] = dest
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
resp['path'] = dest
|
||||
else:
|
||||
changed = False
|
||||
|
||||
# Transmogrify the headers, replacing '-' with '_', since variables dont
|
||||
# work with dashes.
|
||||
# In python3, the headers are title cased. Lowercase them to be
|
||||
# compatible with the python2 behaviour.
|
||||
uresp = {}
|
||||
for key, value in six.iteritems(resp):
|
||||
ukey = key.replace("-", "_").lower()
|
||||
uresp[ukey] = value
|
||||
|
||||
try:
|
||||
uresp['location'] = absolute_location(url, uresp['location'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Default content_encoding to try
|
||||
content_encoding = 'utf-8'
|
||||
if 'content_type' in uresp:
|
||||
content_type, params = cgi.parse_header(uresp['content_type'])
|
||||
if 'charset' in params:
|
||||
content_encoding = params['charset']
|
||||
u_content = to_text(content, encoding=content_encoding)
|
||||
if 'application/json' in content_type or 'text/json' in content_type:
|
||||
try:
|
||||
js = json.loads(u_content)
|
||||
uresp['json'] = js
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
u_content = to_text(content, encoding=content_encoding)
|
||||
|
||||
if resp['status'] not in status_code:
|
||||
uresp['msg'] = 'Status code was not %s: %s' % (status_code, uresp.get('msg', ''))
|
||||
module.fail_json(content=u_content, **uresp)
|
||||
elif return_content:
|
||||
module.exit_json(changed=changed, content=u_content, **uresp)
|
||||
else:
|
||||
module.exit_json(changed=changed, **uresp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,674 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cloudflare_dns
|
||||
author: "Michael Gruener (@mgruener)"
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
version_added: "2.1"
|
||||
short_description: manage Cloudflare DNS records
|
||||
description:
|
||||
- "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)"
|
||||
options:
|
||||
account_api_token:
|
||||
description:
|
||||
- >
|
||||
Account API token. You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://www.cloudflare.com/a/account)
|
||||
required: true
|
||||
account_email:
|
||||
description:
|
||||
- "Account email."
|
||||
required: true
|
||||
port:
|
||||
description: Service port. Required for C(type=SRV)
|
||||
required: false
|
||||
default: null
|
||||
priority:
|
||||
description: Record priority. Required for C(type=MX) and C(type=SRV)
|
||||
required: false
|
||||
default: "1"
|
||||
proto:
|
||||
description: Service protocol. Required for C(type=SRV)
|
||||
required: false
|
||||
choices: [ 'tcp', 'udp' ]
|
||||
default: null
|
||||
proxied:
|
||||
description: Proxy through cloudflare network or just use DNS
|
||||
required: false
|
||||
default: no
|
||||
version_added: "2.3"
|
||||
record:
|
||||
description:
|
||||
- Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name)
|
||||
required: false
|
||||
default: "@"
|
||||
aliases: [ "name" ]
|
||||
service:
|
||||
description: Record service. Required for C(type=SRV)
|
||||
required: false
|
||||
default: null
|
||||
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.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Whether the record(s) should exist or not
|
||||
required: false
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: present
|
||||
timeout:
|
||||
description:
|
||||
- Timeout for Cloudflare API calls
|
||||
required: false
|
||||
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.
|
||||
required: false
|
||||
default: 1 (automatic)
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create. Required if C(state=present)
|
||||
required: false
|
||||
choices: [ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ]
|
||||
default: null
|
||||
value:
|
||||
description:
|
||||
- The record value. Required for C(state=present)
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ "content" ]
|
||||
weight:
|
||||
description: Service weight. Required for C(type=SRV)
|
||||
required: false
|
||||
default: "1"
|
||||
zone:
|
||||
description:
|
||||
- The name of the Zone to work with (e.g. "example.com"). The Zone must already exist.
|
||||
required: true
|
||||
aliases: ["domain"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# create a test.my.com A record to point to 127.0.0.1
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
record: test
|
||||
type: A
|
||||
value: 127.0.0.1
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
register: record
|
||||
|
||||
# create a my.com CNAME record to example.com
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# change it's ttl
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
ttl: 600
|
||||
state: present
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# and delete the record
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: absent
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create a my.com CNAME record to example.com and proxy through cloudflare's network
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
proxied: yes
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create TXT record "test.my.com" with value "unique value"
|
||||
# delete all other TXT records named "test.my.com"
|
||||
- cloudflare_dns:
|
||||
domain: my.com
|
||||
record: test
|
||||
type: TXT
|
||||
value: unique value
|
||||
state: present
|
||||
solo: true
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create a SRV record _foo._tcp.my.com
|
||||
- cloudflare_dns:
|
||||
domain: my.com
|
||||
service: foo
|
||||
proto: tcp
|
||||
port: 3500
|
||||
priority: 10
|
||||
weight: 20
|
||||
type: SRV
|
||||
value: fooserver.my.com
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
record:
|
||||
description: 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: string
|
||||
sample: 192.0.2.91
|
||||
created_on:
|
||||
description: the record creation date
|
||||
returned: success
|
||||
type: string
|
||||
sample: 2016-03-25T19:09:42.516553Z
|
||||
data:
|
||||
description: additional record data
|
||||
returned: success, if type is SRV
|
||||
type: dictionary
|
||||
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: string
|
||||
sample: f9efb0549e96abcb750de63b38c9576e
|
||||
locked:
|
||||
description: No documentation available
|
||||
returned: success
|
||||
type: boolean
|
||||
sample: False
|
||||
meta:
|
||||
description: No documentation available
|
||||
returned: success
|
||||
type: dictionary
|
||||
sample: { auto_added: false }
|
||||
modified_on:
|
||||
description: record modification date
|
||||
returned: success
|
||||
type: string
|
||||
sample: 2016-03-25T19:09:42.516553Z
|
||||
name:
|
||||
description: the record name as FQDN (including _service and _proto for SRV)
|
||||
returned: success
|
||||
type: string
|
||||
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: boolean
|
||||
sample: False
|
||||
proxied:
|
||||
description: whether the record is proxied through cloudflare
|
||||
returned: success
|
||||
type: boolean
|
||||
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: string
|
||||
sample: A
|
||||
zone_id:
|
||||
description: the id of the zone containing the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: abcede0bf9f0066f94029d2e6b73856a
|
||||
zone_name:
|
||||
description: the name of the zone containing the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: sample.com
|
||||
'''
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
# Let snippet from module_utils/basic.py return a proper error in this case
|
||||
pass
|
||||
|
||||
import urllib
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
class CloudflareAPI(object):
|
||||
|
||||
cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
|
||||
changed = False
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.account_api_token = module.params['account_api_token']
|
||||
self.account_email = module.params['account_email']
|
||||
self.port = module.params['port']
|
||||
self.priority = module.params['priority']
|
||||
self.proto = module.params['proto']
|
||||
self.proxied = module.params['proxied']
|
||||
self.record = module.params['record']
|
||||
self.service = 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 = 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('.')
|
||||
|
||||
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 not self.record.endswith(self.zone):
|
||||
self.record = self.record + '.' + self.zone
|
||||
|
||||
def _cf_simple_api_call(self,api_call,method='GET',payload=None):
|
||||
headers = { 'X-Auth-Email': self.account_email,
|
||||
'X-Auth-Key': self.account_api_token,
|
||||
'Content-Type': 'application/json' }
|
||||
data = None
|
||||
if payload:
|
||||
try:
|
||||
data = json.dumps(payload)
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Failed to encode payload as JSON: %s " % str(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(content)
|
||||
except json.JSONDecodeError:
|
||||
error_msg += "; Failed to parse API response: {0}".format(content)
|
||||
|
||||
# received an error status but no data with details on what failed
|
||||
if (info['status'] not in [200,304]) and (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 = '?' + urllib.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 += '?' + urllib.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']:
|
||||
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':
|
||||
content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
|
||||
search_record = params['service'] + '.' + 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']:
|
||||
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']
|
||||
|
||||
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 ('data' in new_record) and ('data' in cur_record):
|
||||
if (cur_record['data'] > new_record['data']) - (cur_record['data'] < new_record['data']):
|
||||
do_update = True
|
||||
if (type == 'CNAME') and (cur_record['content'] != new_record['content']):
|
||||
do_update = True
|
||||
if do_update:
|
||||
if not self.module.check_mode:
|
||||
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 not self.module.check_mode:
|
||||
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(
|
||||
account_api_token = dict(required=True, no_log=True, type='str'),
|
||||
account_email = dict(required=True, type='str'),
|
||||
port = dict(required=False, default=None, type='int'),
|
||||
priority = dict(required=False, default=1, type='int'),
|
||||
proto = dict(required=False, default=None, choices=[ 'tcp', 'udp' ], type='str'),
|
||||
proxied = dict(required=False, default=False, type='bool'),
|
||||
record = dict(required=False, default='@', aliases=['name'], type='str'),
|
||||
service = dict(required=False, default=None, type='str'),
|
||||
solo = dict(required=False, default=None, type='bool'),
|
||||
state = dict(required=False, default='present', choices=['present', 'absent'], type='str'),
|
||||
timeout = dict(required=False, default=30, type='int'),
|
||||
ttl = dict(required=False, default=1, type='int'),
|
||||
type = dict(required=False, default=None, choices=[ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ], type='str'),
|
||||
value = dict(required=False, default=None, aliases=['content'], type='str'),
|
||||
weight = dict(required=False, default=1, type='int'),
|
||||
zone = dict(required=True, default=None, aliases=['domain'], type='str'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
required_if = ([
|
||||
('state','present',['record','type']),
|
||||
('type','MX',['priority','value']),
|
||||
('type','SRV',['port','priority','proto','service','value','weight']),
|
||||
('type','A',['value']),
|
||||
('type','AAAA',['value']),
|
||||
('type','CNAME',['value']),
|
||||
('type','TXT',['value']),
|
||||
('type','NS',['value']),
|
||||
('type','SPF',['value'])
|
||||
]
|
||||
),
|
||||
required_one_of = (
|
||||
[['record','value','type']]
|
||||
)
|
||||
)
|
||||
|
||||
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]})
|
||||
else:
|
||||
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()
|
|
@ -1,352 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: dnsimple
|
||||
version_added: "1.6"
|
||||
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/)"
|
||||
options:
|
||||
account_email:
|
||||
description:
|
||||
- >
|
||||
Account email. If omitted, the env variables DNSIMPLE_EMAIL and 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)
|
||||
required: false
|
||||
default: null
|
||||
|
||||
account_api_token:
|
||||
description:
|
||||
- Account API token. See I(account_email) for info.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
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.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record:
|
||||
description:
|
||||
- Record to add, if blank a record for the domain will be created, supports the wildcard (*)
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_ids:
|
||||
description:
|
||||
- List of records to ensure they either exist or don't exist
|
||||
required: false
|
||||
default: null
|
||||
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create
|
||||
required: false
|
||||
choices: [ 'A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL' ]
|
||||
default: null
|
||||
|
||||
ttl:
|
||||
description:
|
||||
- The TTL to give the new record
|
||||
required: false
|
||||
default: 3600 (one hour)
|
||||
|
||||
value:
|
||||
description:
|
||||
- Record value
|
||||
- "Must be specified when trying to ensure a record exists"
|
||||
required: false
|
||||
default: null
|
||||
|
||||
priority:
|
||||
description:
|
||||
- Record priority
|
||||
required: false
|
||||
default: null
|
||||
|
||||
state:
|
||||
description:
|
||||
- whether the record should exist or not
|
||||
required: false
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: null
|
||||
|
||||
solo:
|
||||
description:
|
||||
- Whether the record should be the only one for that record type and record name. Only use with state=present on a record
|
||||
required: false
|
||||
default: null
|
||||
|
||||
requirements: [ dnsimple ]
|
||||
author: "Alex Coomans (@drcapulet)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# authenticate using email and API token and fetch all domains
|
||||
- dnsimple:
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
delegate_to: localhost
|
||||
|
||||
# fetch my.com domain records
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
register: records
|
||||
|
||||
# delete a domain
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
|
||||
# create a test.my.com A record to point to 127.0.0.01
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: test
|
||||
type: A
|
||||
value: 127.0.0.1
|
||||
delegate_to: localhost
|
||||
register: record
|
||||
|
||||
# and then delete it
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record_ids: '{{ record["id"] }}'
|
||||
delegate_to: localhost
|
||||
|
||||
# create a my.com CNAME record to example.com
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
# change it's ttl
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
ttl: 600
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
# and delete the record
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
import os
|
||||
try:
|
||||
from dnsimple import DNSimple
|
||||
from dnsimple.dnsimple import DNSimpleException
|
||||
HAS_DNSIMPLE = True
|
||||
except ImportError:
|
||||
HAS_DNSIMPLE = False
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
account_email=dict(required=False),
|
||||
account_api_token=dict(required=False, no_log=True),
|
||||
domain=dict(required=False),
|
||||
record=dict(required=False),
|
||||
record_ids=dict(required=False, type='list'),
|
||||
type=dict(required=False, choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO',
|
||||
'POOL']),
|
||||
ttl=dict(required=False, default=3600, type='int'),
|
||||
value=dict(required=False),
|
||||
priority=dict(required=False, type='int'),
|
||||
state=dict(required=False, choices=['present', 'absent']),
|
||||
solo=dict(required=False, type='bool'),
|
||||
),
|
||||
required_together = (
|
||||
['record', 'value']
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
if not HAS_DNSIMPLE:
|
||||
module.fail_json(msg="dnsimple required for this module")
|
||||
|
||||
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'])
|
||||
elif state == 'absent':
|
||||
if dr:
|
||||
if not module.check_mode:
|
||||
client.delete(domain)
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# 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))]
|
||||
|
||||
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['record_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['record_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['prio'] != priority:
|
||||
data = {}
|
||||
if ttl:
|
||||
data['ttl'] = ttl
|
||||
if priority:
|
||||
data['prio'] = 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,
|
||||
'record_type': record_type,
|
||||
'content': value,
|
||||
}
|
||||
if ttl:
|
||||
data['ttl'] = ttl
|
||||
if priority:
|
||||
data['prio'] = priority
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=True, result=client.add_record(str(domain), data)['record'])
|
||||
elif state == 'absent':
|
||||
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)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# 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)
|
||||
elif state == 'absent':
|
||||
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)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
except DNSimpleException:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Unable to contact DNSimple: %s" % e.message)
|
||||
|
||||
module.fail_json(msg="Unknown what you wanted me to do")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,411 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: dnsmadeeasy
|
||||
version_added: "1.3"
|
||||
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
|
||||
default: null
|
||||
|
||||
account_secret:
|
||||
description:
|
||||
- Account Secret Key.
|
||||
required: true
|
||||
default: null
|
||||
|
||||
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
|
||||
default: null
|
||||
|
||||
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.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_type:
|
||||
description:
|
||||
- Record type.
|
||||
required: false
|
||||
choices: [ 'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT' ]
|
||||
default: null
|
||||
|
||||
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)
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_ttl:
|
||||
description:
|
||||
- record's "Time to live". Number of seconds the record remains cached in DNS servers.
|
||||
required: false
|
||||
default: 1800
|
||||
|
||||
state:
|
||||
description:
|
||||
- whether the record should exist or not
|
||||
required: true
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: null
|
||||
|
||||
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.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: 1.5.1
|
||||
|
||||
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) in the "result" element when 'state' is set to 'present'. This value can be be registered and used in your playbooks.
|
||||
|
||||
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
|
||||
state: absent
|
||||
record_name: test
|
||||
'''
|
||||
|
||||
# ============================================
|
||||
# DNSMadeEasy module specific support methods.
|
||||
#
|
||||
|
||||
import urllib
|
||||
|
||||
IMPORT_ERROR = None
|
||||
try:
|
||||
import json
|
||||
from time import strftime, gmtime
|
||||
import hashlib
|
||||
import hmac
|
||||
except ImportError:
|
||||
e = get_exception()
|
||||
IMPORT_ERROR = str(e)
|
||||
|
||||
class DME2:
|
||||
|
||||
def __init__(self, apikey, secret, domain, module):
|
||||
self.module = module
|
||||
|
||||
self.api = apikey
|
||||
self.secret = secret
|
||||
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
|
||||
|
||||
# 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'
|
||||
|
||||
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):
|
||||
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, basestring):
|
||||
data = urllib.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 ["A", "AAAA", "CNAME", "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 ["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')
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 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),
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
record_name=dict(required=False),
|
||||
record_type=dict(required=False, choices=[
|
||||
'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT']),
|
||||
record_value=dict(required=False),
|
||||
record_ttl=dict(required=False, default=1800, type='int'),
|
||||
validate_certs = dict(default='yes', type='bool'),
|
||||
),
|
||||
required_together=(
|
||||
['record_value', 'record_ttl', 'record_type']
|
||||
)
|
||||
)
|
||||
|
||||
if IMPORT_ERROR:
|
||||
module.fail_json(msg="Import Error: " + IMPORT_ERROR)
|
||||
|
||||
DME = DME2(module.params["account_key"], module.params[
|
||||
"account_secret"], module.params["domain"], 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]
|
||||
|
||||
# Compare new record against existing one
|
||||
changed = False
|
||||
if current_record:
|
||||
for i in new_record:
|
||||
if str(current_record[i]) != str(new_record[i]):
|
||||
changed = True
|
||||
new_record['id'] = str(current_record['id'])
|
||||
|
||||
# Follow Keyword Controlled Behavior
|
||||
if state == 'present':
|
||||
# return the record if no value is specified
|
||||
if not "value" 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=current_record)
|
||||
|
||||
# create record as it does not exist
|
||||
if not current_record:
|
||||
record = DME.createRecord(DME.prepareRecord(new_record))
|
||||
module.exit_json(changed=True, result=record)
|
||||
|
||||
# update the record
|
||||
if changed:
|
||||
DME.updateRecord(
|
||||
current_record['id'], DME.prepareRecord(new_record))
|
||||
module.exit_json(changed=True, result=new_record)
|
||||
|
||||
# return the record (no changes)
|
||||
module.exit_json(changed=False, result=current_record)
|
||||
|
||||
elif state == 'absent':
|
||||
# delete the record 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=False)
|
||||
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,262 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'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.
|
||||
version_added: "2.2"
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the record.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- State of the resource.
|
||||
required: false
|
||||
default: 'present'
|
||||
choices: [ 'present', 'absent' ]
|
||||
api_key:
|
||||
description:
|
||||
- API key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_secret:
|
||||
description:
|
||||
- Secret key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_timeout:
|
||||
description:
|
||||
- HTTP timeout to Exoscale DNS API.
|
||||
required: false
|
||||
default: 10
|
||||
api_region:
|
||||
description:
|
||||
- Name of the ini section in the C(cloustack.ini) file.
|
||||
required: false
|
||||
default: cloudstack
|
||||
validate_certs:
|
||||
description:
|
||||
- Validate SSL certs of the Exoscale DNS API.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
notes:
|
||||
- As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack.
|
||||
The config is read from several locations, in the following order.
|
||||
The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables.
|
||||
A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file,
|
||||
A C(cloudstack.ini) file in the current working directory.
|
||||
A C(.cloudstack.ini) file in the users home directory.
|
||||
Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini).
|
||||
Use the argument C(api_region) to select the section name, default section is C(cloudstack).
|
||||
- This module does not support multiple A records and will complain properly if you try.
|
||||
- More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/.
|
||||
- This module supports check mode and diff.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a domain.
|
||||
- local_action:
|
||||
module: exo_dns_domain
|
||||
name: example.com
|
||||
|
||||
# Remove a domain.
|
||||
- local_action:
|
||||
module: 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: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
expires_on:
|
||||
description: When the domain expires
|
||||
returned: success
|
||||
type: string
|
||||
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: string
|
||||
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: string
|
||||
sample: "hosted"
|
||||
token:
|
||||
description: Token
|
||||
returned: success
|
||||
type: string
|
||||
sample: "r4NzTRp6opIeFKfaFYvOd6MlhGyD07jl"
|
||||
unicode_name:
|
||||
description: Domain name as unicode
|
||||
returned: success
|
||||
type: string
|
||||
sample: "example.com"
|
||||
updated_at:
|
||||
description: When the domain was updated last.
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
user_id:
|
||||
description: ID of the user
|
||||
returned: success
|
||||
type: int
|
||||
sample: null
|
||||
whois_protected:
|
||||
description: Wheter the whois is protected or not
|
||||
returned: success
|
||||
type: bool
|
||||
sample: false
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.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(required=True),
|
||||
state=dict(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()
|
|
@ -1,415 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: exo_dns_record
|
||||
short_description: Manages DNS records on Exoscale DNS.
|
||||
description:
|
||||
- Create, update and delete records.
|
||||
version_added: "2.2"
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the record.
|
||||
required: false
|
||||
default: ""
|
||||
domain:
|
||||
description:
|
||||
- Domain the record is related to.
|
||||
required: true
|
||||
record_type:
|
||||
description:
|
||||
- Type of the record.
|
||||
required: false
|
||||
default: A
|
||||
choices: ['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL']
|
||||
aliases: ['rtype', 'type']
|
||||
content:
|
||||
description:
|
||||
- Content of the record.
|
||||
- Required if C(state=present) or C(name="")
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['value', 'address']
|
||||
ttl:
|
||||
description:
|
||||
- TTL of the record in seconds.
|
||||
required: false
|
||||
default: 3600
|
||||
prio:
|
||||
description:
|
||||
- Priority of the record.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['priority']
|
||||
multiple:
|
||||
description:
|
||||
- Whether there are more than one records with similar C(name).
|
||||
- Only allowed with C(record_type=A).
|
||||
- C(content) will not be updated as it is used as key to find the record.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['priority']
|
||||
state:
|
||||
description:
|
||||
- State of the record.
|
||||
required: false
|
||||
default: 'present'
|
||||
choices: [ 'present', 'absent' ]
|
||||
api_key:
|
||||
description:
|
||||
- API key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_secret:
|
||||
description:
|
||||
- Secret key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_timeout:
|
||||
description:
|
||||
- HTTP timeout to Exoscale DNS API.
|
||||
required: false
|
||||
default: 10
|
||||
api_region:
|
||||
description:
|
||||
- Name of the ini section in the C(cloustack.ini) file.
|
||||
required: false
|
||||
default: cloudstack
|
||||
validate_certs:
|
||||
description:
|
||||
- Validate SSL certs of the Exoscale DNS API.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
notes:
|
||||
- As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack.
|
||||
The config is read from several locations, in the following order.
|
||||
The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables.
|
||||
A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file,
|
||||
A C(cloudstack.ini) file in the current working directory.
|
||||
A C(.cloudstack.ini) file in the users home directory.
|
||||
Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini).
|
||||
Use the argument C(api_region) to select the section name, default section is C(cloudstack).
|
||||
- This module does not support multiple A records and will complain properly if you try.
|
||||
- More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/.
|
||||
- This module supports check mode and diff.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create or update an A record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.4
|
||||
|
||||
# Update an existing A record with a new IP.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.5
|
||||
|
||||
# Create another A record with same name.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.6
|
||||
multiple: yes
|
||||
|
||||
# Create or update a CNAME record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: www
|
||||
domain: example.com
|
||||
record_type: CNAME
|
||||
content: web-vm-1
|
||||
|
||||
# Create or update a MX record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
domain: example.com
|
||||
record_type: MX
|
||||
content: mx1.example.com
|
||||
prio: 10
|
||||
|
||||
# delete a MX record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
domain: example.com
|
||||
record_type: MX
|
||||
content: mx1.example.com
|
||||
state: absent
|
||||
|
||||
# Remove a record.
|
||||
- local_action:
|
||||
module: 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: string
|
||||
sample: 1.2.3.4
|
||||
created_at:
|
||||
description: When the record was created
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
domain:
|
||||
description: Name of the domain
|
||||
returned: success
|
||||
type: string
|
||||
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: string
|
||||
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: string
|
||||
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: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.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')
|
||||
if self.multiple and self.record_type != 'A':
|
||||
self.module.fail_json(msg="Multiple is only usable with record_type A")
|
||||
|
||||
self.content = self.module.params.get('content')
|
||||
if self.content and self.record_type != 'TXT':
|
||||
self.content = self.content.lower()
|
||||
|
||||
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")
|
||||
|
||||
record = None
|
||||
for r in records:
|
||||
found_record = None
|
||||
if r['record']['record_type'] == self.record_type:
|
||||
r_name = r['record']['name'].lower()
|
||||
r_content = r['record']['content'].lower()
|
||||
|
||||
# there are multiple A records but we found an exact match
|
||||
if self.multiple and self.name == r_name and self.content == r_content:
|
||||
record = r
|
||||
break
|
||||
|
||||
# We do not expect to found more then one record with that content
|
||||
if not self.multiple and not self.name and self.content == r_content:
|
||||
found_record = r
|
||||
|
||||
# We do not expect to found more then one record with that name
|
||||
elif not self.multiple and self.name and self.name == r_name:
|
||||
found_record = r
|
||||
|
||||
if record and found_record:
|
||||
self.module.fail_json(msg="More than one record with your params. Use multiple=yes for more than one A record.")
|
||||
if found_record:
|
||||
record = found_record
|
||||
return record
|
||||
|
||||
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(default=""),
|
||||
record_type=dict(choices=EXO_RECORD_TYPES, aliases=['rtype', 'type'], default='A'),
|
||||
content=dict(aliases=['value', 'address']),
|
||||
multiple=(dict(type='bool', default=False)),
|
||||
ttl=dict(type='int', default=3600),
|
||||
prio=dict(type='int', aliases=['priority']),
|
||||
domain=dict(required=True),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=exo_dns_required_together(),
|
||||
required_if=[
|
||||
['state', 'present', ['content']],
|
||||
['name', '', ['content']],
|
||||
],
|
||||
required_one_of=[
|
||||
['content', 'name'],
|
||||
],
|
||||
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()
|
|
@ -1,400 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Ravi Bhure <ravibhure@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: haproxy
|
||||
version_added: "1.9"
|
||||
short_description: Enable, disable, and set weights for HAProxy backend servers using socket commands.
|
||||
author: "Ravi Bhure (@ravibhure)"
|
||||
description:
|
||||
- Enable, disable, and set weights for HAProxy backend servers using socket
|
||||
commands.
|
||||
notes:
|
||||
- Enable and disable 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 http://haproxy.1wt.eu/download/1.5/doc/configuration.txt.
|
||||
options:
|
||||
backend:
|
||||
description:
|
||||
- Name of the HAProxy backend pool.
|
||||
required: false
|
||||
default: auto-detected
|
||||
host:
|
||||
description:
|
||||
- Name of the backend host to change.
|
||||
required: true
|
||||
default: null
|
||||
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.
|
||||
required: false
|
||||
default: false
|
||||
socket:
|
||||
description:
|
||||
- Path to the HAProxy socket file.
|
||||
required: false
|
||||
default: /var/run/haproxy.sock
|
||||
state:
|
||||
description:
|
||||
- Desired state of the provided backend host.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "enabled", "disabled" ]
|
||||
fail_on_not_found:
|
||||
description:
|
||||
- Fail whenever trying to enable/disable a backend host that does not exist
|
||||
required: false
|
||||
default: false
|
||||
version_added: "2.2"
|
||||
wait:
|
||||
description:
|
||||
- Wait until the server reports a status of 'UP' when `state=enabled`, or
|
||||
status of 'MAINT' when `state=disabled`.
|
||||
required: false
|
||||
default: false
|
||||
version_added: "2.0"
|
||||
wait_interval:
|
||||
description:
|
||||
- Number of seconds to wait between retries.
|
||||
required: false
|
||||
default: 5
|
||||
version_added: "2.0"
|
||||
wait_retries:
|
||||
description:
|
||||
- Number of times to check for status after changing the state.
|
||||
required: false
|
||||
default: 25
|
||||
version_added: "2.0"
|
||||
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.
|
||||
required: false
|
||||
default: null
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# disable server in 'www' backend pool
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
|
||||
# disable server without backend pool name (apply to all available backend pool)
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
|
||||
# disable server, provide socket file
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
socket: /var/run/haproxy.sock
|
||||
backend: www
|
||||
|
||||
# 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
|
||||
|
||||
# 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: true
|
||||
|
||||
# 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
|
||||
|
||||
# enable server in 'www' backend pool
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
|
||||
# enable server in 'www' backend pool wait until healthy
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
wait: yes
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
'''
|
||||
|
||||
import socket
|
||||
import csv
|
||||
import time
|
||||
from string import Template
|
||||
|
||||
|
||||
DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock"
|
||||
RECV_SIZE = 1024
|
||||
ACTION_CHOICES = ['enabled', 'disabled']
|
||||
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.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('%s\n' % cmd)
|
||||
result = ''
|
||||
buf = ''
|
||||
buf = self.client.recv(RECV_SIZE)
|
||||
while buf:
|
||||
result += buf
|
||||
buf = self.client.recv(RECV_SIZE)
|
||||
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 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 or self.wait) and state is None:
|
||||
self.module.fail_json(msg="The specified backend '%s/%s' was not found!" % (backend, svname))
|
||||
|
||||
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']},
|
||||
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
|
||||
if state[0]['status'] == status:
|
||||
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 act(self):
|
||||
"""
|
||||
Figure out what you want to do from ansible, and then do it.
|
||||
"""
|
||||
# Get the state before the run
|
||||
state_before = self.get_state_for(self.backend, self.host)
|
||||
self.command_results['state_before'] = state_before
|
||||
|
||||
# toggle enable/disbale server
|
||||
if self.state == 'enabled':
|
||||
self.enabled(self.host, self.backend, self.weight)
|
||||
elif self.state == 'disabled':
|
||||
self.disabled(self.host, self.backend, self.shutdown_sessions)
|
||||
else:
|
||||
self.module.fail_json(msg="unknown state specified: '%s'" % self.state)
|
||||
|
||||
# Get the state after the run
|
||||
state_after = self.get_state_for(self.backend, self.host)
|
||||
self.command_results['state_after'] = state_after
|
||||
|
||||
# Report change status
|
||||
if state_before != state_after:
|
||||
self.command_results['changed'] = True
|
||||
self.module.exit_json(**self.command_results)
|
||||
else:
|
||||
self.command_results['changed'] = False
|
||||
self.module.exit_json(**self.command_results)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# load ansible module object
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(required=True, default=None, choices=ACTION_CHOICES),
|
||||
host=dict(required=True, default=None),
|
||||
backend=dict(required=False, default=None),
|
||||
weight=dict(required=False, default=None),
|
||||
socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION),
|
||||
shutdown_sessions=dict(required=False, default=False, type='bool'),
|
||||
fail_on_not_found=dict(required=False, default=False, type='bool'),
|
||||
wait=dict(required=False, default=False, type='bool'),
|
||||
wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'),
|
||||
wait_interval=dict(required=False, default=WAIT_INTERVAL, type='int'),
|
||||
),
|
||||
)
|
||||
|
||||
if not socket:
|
||||
module.fail_json(msg="unable to locate haproxy socket")
|
||||
|
||||
ansible_haproxy = HAProxy(module)
|
||||
ansible_haproxy.act()
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,119 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
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.
|
||||
version_added: '2.0'
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
api_url:
|
||||
description:
|
||||
- URL of the ipify.org API service.
|
||||
- C(?format=json) will be appended per default.
|
||||
required: false
|
||||
default: 'https://api.ipify.org'
|
||||
timeout:
|
||||
description:
|
||||
- HTTP connection timeout in seconds.
|
||||
required: false
|
||||
default: 10
|
||||
version_added: "2.3"
|
||||
notes:
|
||||
- "Visit https://www.ipify.org to get more information."
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# 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 = '''
|
||||
---
|
||||
ipify_public_ip:
|
||||
description: Public IP of the internet gateway.
|
||||
returned: success
|
||||
type: string
|
||||
sample: 1.2.3.4
|
||||
'''
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
# Let snippet from module_utils/basic.py return a proper error in this case
|
||||
pass
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
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(response.read())
|
||||
result['ipify_public_ip'] = data.get('ip')
|
||||
return result
|
||||
|
||||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
api_url=dict(default='https://api.ipify.org'),
|
||||
timeout=dict(type='int', default=10),
|
||||
),
|
||||
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()
|
|
@ -1,142 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2016, Aleksei Kostiuk <unitoff@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'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"
|
||||
version_added: "2.3"
|
||||
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: string
|
||||
sample: "8.8.8.8"
|
||||
hostname:
|
||||
description: Domain name
|
||||
type: string
|
||||
sample: "google-public-dns-a.google.com"
|
||||
country:
|
||||
description: ISO 3166-1 alpha-2 country code
|
||||
type: string
|
||||
sample: "US"
|
||||
region:
|
||||
description: State or province name
|
||||
type: string
|
||||
sample: "California"
|
||||
city:
|
||||
description: City name
|
||||
type: string
|
||||
sample: "Mountain View"
|
||||
loc:
|
||||
description: Latitude and Longitude of the location
|
||||
type: string
|
||||
sample: "37.3860,-122.0838"
|
||||
org:
|
||||
description: "organization's name"
|
||||
type: string
|
||||
sample: "AS3356 Level 3 Communications, Inc."
|
||||
postal:
|
||||
description: Postal code
|
||||
type: string
|
||||
sample: "94035"
|
||||
'''
|
||||
|
||||
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 {} 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)
|
||||
|
||||
from ansible.module_utils.basic import * # NOQA
|
||||
from ansible.module_utils.urls import * # NOQA
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,359 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Peter Sagerson <psagers@ignorare.net>
|
||||
# (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
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.
|
||||
version_added: '2.3'
|
||||
author:
|
||||
- Jiri Tyr (@jtyr)
|
||||
requirements:
|
||||
- python-ldap
|
||||
options:
|
||||
bind_dn:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- A DN to bind with. If this is omitted, we'll try a SASL bind with
|
||||
the EXTERNAL mechanism. If this is blank, we'll use an anonymous
|
||||
bind.
|
||||
bind_pw:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- The password to use with I(bind_dn).
|
||||
dn:
|
||||
required: true
|
||||
description:
|
||||
- The DN of the entry to modify.
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- The name of the attribute to modify.
|
||||
server_uri:
|
||||
required: false
|
||||
default: ldapi:///
|
||||
description:
|
||||
- A URI to the LDAP server. The default value lets the underlying
|
||||
LDAP client library look for a UNIX domain socket in its default
|
||||
location.
|
||||
start_tls:
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
default: 'no'
|
||||
description:
|
||||
- If true, we'll use the START_TLS LDAP extension.
|
||||
state:
|
||||
required: false
|
||||
choices: [present, absent, exact]
|
||||
default: present
|
||||
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 empty, all values for this
|
||||
attribute will be removed.
|
||||
values:
|
||||
required: true
|
||||
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).
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = """
|
||||
- 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 = """
|
||||
modlist:
|
||||
description: list of modified parameters
|
||||
returned: success
|
||||
type: list
|
||||
sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]'
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
try:
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
|
||||
HAS_LDAP = True
|
||||
except ImportError:
|
||||
HAS_LDAP = False
|
||||
|
||||
|
||||
class LdapAttr(object):
|
||||
def __init__(self, module):
|
||||
# Shortcuts
|
||||
self.module = module
|
||||
self.bind_dn = self.module.params['bind_dn']
|
||||
self.bind_pw = self.module.params['bind_pw']
|
||||
self.dn = self.module.params['dn']
|
||||
self.name = self.module.params['name']
|
||||
self.server_uri = self.module.params['server_uri']
|
||||
self.start_tls = self.module.params['start_tls']
|
||||
self.state = self.module.params['state']
|
||||
|
||||
# Normalize values
|
||||
if isinstance(self.module.params['values'], list):
|
||||
self.values = map(str, self.module.params['values'])
|
||||
else:
|
||||
self.values = [str(self.module.params['values'])]
|
||||
|
||||
# Establish connection
|
||||
self.connection = self._connect_to_ldap()
|
||||
|
||||
def add(self):
|
||||
values_to_add = 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 = 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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(
|
||||
msg="Cannot search for attribute %s" % self.name,
|
||||
details=str(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 _connect_to_ldap(self):
|
||||
connection = ldap.initialize(self.server_uri)
|
||||
|
||||
if self.start_tls:
|
||||
try:
|
||||
connection.start_tls_s()
|
||||
except ldap.LDAPError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Cannot start TLS.", details=str(e))
|
||||
|
||||
try:
|
||||
if self.bind_dn is not None:
|
||||
connection.simple_bind_s(self.bind_dn, self.bind_pw)
|
||||
else:
|
||||
connection.sasl_interactive_bind_s('', ldap.sasl.external())
|
||||
except ldap.LDAPError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(
|
||||
msg="Cannot bind to the server.", details=str(e))
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'bind_dn': dict(default=None),
|
||||
'bind_pw': dict(default='', no_log=True),
|
||||
'dn': dict(required=True),
|
||||
'name': dict(required=True),
|
||||
'params': dict(type='dict'),
|
||||
'server_uri': dict(default='ldapi:///'),
|
||||
'start_tls': dict(default=False, type='bool'),
|
||||
'state': dict(
|
||||
default='present',
|
||||
choices=['present', 'absent', 'exact']),
|
||||
'values': dict(required=True, type='raw'),
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not HAS_LDAP:
|
||||
module.fail_json(
|
||||
msg="Missing requried 'ldap' module (pip install python-ldap)")
|
||||
|
||||
# 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:
|
||||
e = get_exception()
|
||||
module.fail_json(
|
||||
msg="Attribute action failed.", details=str(e))
|
||||
|
||||
module.exit_json(changed=changed, modlist=modlist)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,324 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Peter Sagerson <psagers@ignorare.net>
|
||||
# (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'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).
|
||||
version_added: '2.3'
|
||||
author:
|
||||
- Jiri Tyr (@jtyr)
|
||||
requirements:
|
||||
- python-ldap
|
||||
options:
|
||||
bind_dn:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- A DN to bind with. If this is omitted, we'll try a SASL bind with
|
||||
the EXTERNAL mechanism. If this is blank, we'll use an anonymous
|
||||
bind.
|
||||
bind_pw:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- The password to use with I(bind_dn).
|
||||
dn:
|
||||
required: true
|
||||
description:
|
||||
- The DN of the entry to add or remove.
|
||||
attributes:
|
||||
required: false
|
||||
default: null
|
||||
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:
|
||||
required: false
|
||||
default: null
|
||||
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:
|
||||
required: false
|
||||
default: null
|
||||
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).
|
||||
server_uri:
|
||||
required: false
|
||||
default: ldapi:///
|
||||
description:
|
||||
- A URI to the LDAP server. The default value lets the underlying
|
||||
LDAP client library look for a UNIX domain socket in its default
|
||||
location.
|
||||
start_tls:
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
default: 'no'
|
||||
description:
|
||||
- If true, we'll use the START_TLS LDAP extension.
|
||||
state:
|
||||
required: false
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
description:
|
||||
- The target state of the entry.
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
try:
|
||||
import ldap
|
||||
import ldap.modlist
|
||||
import ldap.sasl
|
||||
|
||||
HAS_LDAP = True
|
||||
except ImportError:
|
||||
HAS_LDAP = False
|
||||
|
||||
|
||||
class LdapEntry(object):
|
||||
def __init__(self, module):
|
||||
# Shortcuts
|
||||
self.module = module
|
||||
self.bind_dn = self.module.params['bind_dn']
|
||||
self.bind_pw = self.module.params['bind_pw']
|
||||
self.dn = self.module.params['dn']
|
||||
self.server_uri = self.module.params['server_uri']
|
||||
self.start_tls = self.module.params['start_tls']
|
||||
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()
|
||||
|
||||
# Establish connection
|
||||
self.connection = self._connect_to_ldap()
|
||||
|
||||
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] = value
|
||||
else:
|
||||
attrs[name].append(str(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 _connect_to_ldap(self):
|
||||
connection = ldap.initialize(self.server_uri)
|
||||
|
||||
if self.start_tls:
|
||||
try:
|
||||
connection.start_tls_s()
|
||||
except ldap.LDAPError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Cannot start TLS.", details=str(e))
|
||||
|
||||
try:
|
||||
if self.bind_dn is not None:
|
||||
connection.simple_bind_s(self.bind_dn, self.bind_pw)
|
||||
else:
|
||||
connection.sasl_interactive_bind_s('', ldap.sasl.external())
|
||||
except ldap.LDAPError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(
|
||||
msg="Cannot bind to the server.", details=str(e))
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'attributes': dict(default={}, type='dict'),
|
||||
'bind_dn': dict(),
|
||||
'bind_pw': dict(default='', no_log=True),
|
||||
'dn': dict(required=True),
|
||||
'objectClass': dict(type='raw'),
|
||||
'params': dict(type='dict'),
|
||||
'server_uri': dict(default='ldapi:///'),
|
||||
'start_tls': dict(default=False, type='bool'),
|
||||
'state': dict(default='present', choices=['present', 'absent']),
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not HAS_LDAP:
|
||||
module.fail_json(
|
||||
msg="Missing requried 'ldap' module (pip install python-ldap).")
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
# Chek 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'], basestring) 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:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Entry action failed.", details=str(e))
|
||||
|
||||
module.exit_json(changed=(action is not None))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,94 +0,0 @@
|
|||
#!/usr/bin/python -tt
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: lldp
|
||||
requirements: [ lldpctl ]
|
||||
version_added: 1.6
|
||||
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']['ifalias'] }}"
|
||||
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"}
|
||||
|
||||
'''
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
def gather_lldp():
|
||||
cmd = ['lldpctl', '-f', 'keyvalue']
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
output_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()
|
||||
try:
|
||||
data = {'lldp': lldp_output['lldp']}
|
||||
module.exit_json(ansible_facts=data)
|
||||
except TypeError:
|
||||
module.fail_json(msg="lldpctl command failed. is lldpd running?")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
File diff suppressed because it is too large
Load diff
|
@ -1,364 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
"""
|
||||
Ansible module to manage DNS records using dnspython
|
||||
(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
|
||||
|
||||
This file is part of Ansible
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: nsupdate
|
||||
|
||||
short_description: Manage DNS records.
|
||||
description:
|
||||
- Create, update and remove DNS records using DDNS updates
|
||||
- DDNS works well with both bind and Microsoft DNS (see https://technet.microsoft.com/en-us/library/cc961412.aspx)
|
||||
version_added: "2.3"
|
||||
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.
|
||||
required: true
|
||||
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', 'hamc-sha384',
|
||||
'hmac-sha512']
|
||||
default: 'hmac-md5'
|
||||
zone:
|
||||
description:
|
||||
- DNS record will be modified on this C(zone).
|
||||
required: true
|
||||
record:
|
||||
description:
|
||||
- Sets the DNS record to modify.
|
||||
required: true
|
||||
type:
|
||||
description:
|
||||
- Sets the record type.
|
||||
default: 'A'
|
||||
ttl:
|
||||
description:
|
||||
- Sets the record TTL.
|
||||
default: 3600
|
||||
value:
|
||||
description:
|
||||
- Sets the record value.
|
||||
default: None
|
||||
|
||||
'''
|
||||
|
||||
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: 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
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: If module has modified record
|
||||
returned: success
|
||||
type: string
|
||||
record:
|
||||
description: DNS record
|
||||
returned: success
|
||||
type: string
|
||||
sample: 'ansible'
|
||||
ttl:
|
||||
description: DNS record TTL
|
||||
returned: success
|
||||
type: int
|
||||
sample: 86400
|
||||
type:
|
||||
description: DNS record type
|
||||
returned: success
|
||||
type: string
|
||||
sample: 'CNAME'
|
||||
value:
|
||||
description: DNS record value
|
||||
returned: success
|
||||
type: string
|
||||
sample: '192.168.1.1'
|
||||
zone:
|
||||
description: DNS record zone
|
||||
returned: success
|
||||
type: string
|
||||
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: string
|
||||
sample: 'REFUSED'
|
||||
'''
|
||||
|
||||
from binascii import Error as binascii_error
|
||||
from socket import error as socket_error
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
try:
|
||||
import dns.update
|
||||
import dns.query
|
||||
import dns.tsigkeyring
|
||||
import dns.message
|
||||
import dns.resolver
|
||||
|
||||
HAVE_DNSPYTHON = True
|
||||
except ImportError:
|
||||
HAVE_DNSPYTHON = False
|
||||
|
||||
|
||||
class RecordManager(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
if module.params['zone'][-1] != '.':
|
||||
self.zone = module.params['zone'] + '.'
|
||||
else:
|
||||
self.zone = module.params['zone']
|
||||
|
||||
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:
|
||||
e = get_exception()
|
||||
module.fail_json(msg='TSIG key error: %s' % str(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']
|
||||
|
||||
self.dns_rc = 0
|
||||
|
||||
def __do_update(self, update):
|
||||
response = None
|
||||
try:
|
||||
response = dns.query.tcp(update, self.module.params['server'], timeout=10)
|
||||
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature):
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, str(e)))
|
||||
except (socket_error, dns.exception.Timeout):
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, str(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)
|
||||
try:
|
||||
update.add(self.module.params['record'],
|
||||
self.module.params['ttl'],
|
||||
self.module.params['type'],
|
||||
self.module.params['value'])
|
||||
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.replace(self.module.params['record'],
|
||||
self.module.params['ttl'],
|
||||
self.module.params['type'],
|
||||
self.module.params['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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg='Record error: {}'.format(str(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
|
||||
try:
|
||||
update.present(self.module.params['record'], self.module.params['type'], self.module.params['value'])
|
||||
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:
|
||||
return 1
|
||||
else:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
tsig_algs = ['HMAC-MD5.SIG-ALG.REG.INT', 'hmac-md5', 'hmac-sha1', 'hmac-sha224',
|
||||
'hmac-sha256', 'hamc-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'),
|
||||
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=True, 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='str')
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not HAVE_DNSPYTHON:
|
||||
module.fail_json(msg='python library dnspython required: pip install dnspython')
|
||||
|
||||
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=module.params['value'])
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,331 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to configure DHCPd hosts using OMAPI protocol
|
||||
(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/
|
||||
|
||||
This file is part of Ansible
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: omapi_host
|
||||
|
||||
short_description: Setup OMAPI hosts.
|
||||
description:
|
||||
- Create, update and remove OMAPI hosts into compatible DHCPd servers.
|
||||
version_added: "2.3"
|
||||
requirements:
|
||||
- pypureomapi
|
||||
author: "Loic Blot (@nerzhul)"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Create or remove OMAPI host.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
description:
|
||||
- Sets the host lease hostname (mandatory if state=present).
|
||||
default: None
|
||||
host:
|
||||
description:
|
||||
- Sets OMAPI server host to interact with.
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- Sets the OMAPI server port to interact with.
|
||||
default: 7911
|
||||
key_name:
|
||||
description:
|
||||
- Sets the TSIG key name for authenticating against OMAPI server.
|
||||
required: true
|
||||
key:
|
||||
description:
|
||||
- Sets the TSIG key content for authenticating against OMAPI server.
|
||||
required: true
|
||||
macaddr:
|
||||
description:
|
||||
- Sets the lease host MAC address.
|
||||
required: true
|
||||
ip:
|
||||
description:
|
||||
- Sets the lease host IP address.
|
||||
required: false
|
||||
default: None
|
||||
statements:
|
||||
description:
|
||||
- Attach a list of OMAPI DHCP statements with host lease (without ending semicolon).
|
||||
required: false
|
||||
default: []
|
||||
ddns:
|
||||
description:
|
||||
- Enable dynamic DNS updates for this host.
|
||||
required: false
|
||||
default: false
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- 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
|
||||
|
||||
- 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
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: If module has modified a host
|
||||
returned: success
|
||||
type: string
|
||||
lease:
|
||||
description: dictionnary containing host informations
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
ip-address:
|
||||
description: IP address, if there is.
|
||||
returned: success
|
||||
type: string
|
||||
sample: '192.168.1.5'
|
||||
hardware-address:
|
||||
description: MAC address
|
||||
returned: success
|
||||
type: string
|
||||
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: string
|
||||
sample: 'mydesktop'
|
||||
'''
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import AnsibleModule, get_exception, to_bytes
|
||||
from ansible.module_utils.six import iteritems
|
||||
import socket
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
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_found = False
|
||||
|
||||
|
||||
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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Unable to open OMAPI connection. Ensure 'host', 'port', 'key' and 'key_name' "
|
||||
"are valid. Exception was: %s" % e)
|
||||
except socket.error:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Unable to connect to OMAPI server: %s" % 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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Invalid statements found: %s" % 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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="OMAPI error: %s" % 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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="OMAPI error: %s" % 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:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="OMAPI error: %s" % e)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(required=True, type='str', choices=['present', 'absent']),
|
||||
host=dict(type='str', default="localhost"),
|
||||
port=dict(type='int', default=7911),
|
||||
key_name=dict(required=True, type='str', default=None),
|
||||
key=dict(required=True, type='str', default=None, no_log=True),
|
||||
macaddr=dict(required=True, type='str', default=None),
|
||||
hostname=dict(type='str', default=None, aliases=['name']),
|
||||
ip=dict(type='str', default=None),
|
||||
ddns=dict(type='bool', default=False),
|
||||
statements=dict(type='list', default=[])
|
||||
),
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
if not pureomapi_found:
|
||||
module.fail_json(msg="pypureomapi library is required by this module.")
|
||||
|
||||
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:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="OMAPI input value error: %s" % e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,381 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# This file is part of Networklore's snmp library for Ansible
|
||||
#
|
||||
# The module 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.
|
||||
#
|
||||
# The module 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/>.
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: snmp_facts
|
||||
version_added: "1.9"
|
||||
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 {{inventory_hostname}})
|
||||
required: true
|
||||
version:
|
||||
description:
|
||||
- SNMP Version to use, v2/v2c or v3
|
||||
choices: [ 'v2', 'v2c', 'v3' ]
|
||||
required: true
|
||||
community:
|
||||
description:
|
||||
- The SNMP community string, required if version is v2/v2c
|
||||
required: false
|
||||
level:
|
||||
description:
|
||||
- Authentication level, required if version is v3
|
||||
choices: [ 'authPriv', 'authNoPriv' ]
|
||||
required: false
|
||||
username:
|
||||
description:
|
||||
- Username for SNMPv3, required if version is v3
|
||||
required: false
|
||||
integrity:
|
||||
description:
|
||||
- Hashing algorithm, required if version is v3
|
||||
choices: [ 'md5', 'sha' ]
|
||||
required: false
|
||||
authkey:
|
||||
description:
|
||||
- Authentication key, required if version is v3
|
||||
required: false
|
||||
privacy:
|
||||
description:
|
||||
- Encryption algorithm, required if level is authPriv
|
||||
choices: [ 'des', 'aes' ]
|
||||
required: false
|
||||
privkey:
|
||||
description:
|
||||
- Encryption key, required if version is authPriv
|
||||
required: false
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather facts with SNMP version 2
|
||||
- snmp_facts:
|
||||
host: '{{ inventory_hostname }}'
|
||||
version: 2c
|
||||
community: public
|
||||
delegate_to: local
|
||||
|
||||
# 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
|
||||
'''
|
||||
|
||||
import binascii
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
has_pysnmp = True
|
||||
except:
|
||||
has_pysnmp = False
|
||||
|
||||
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(required=True),
|
||||
version=dict(required=True, choices=['v2', 'v2c', 'v3']),
|
||||
community=dict(required=False, default=False),
|
||||
username=dict(required=False),
|
||||
level=dict(required=False, choices=['authNoPriv', 'authPriv']),
|
||||
integrity=dict(required=False, choices=['md5', 'sha']),
|
||||
privacy=dict(required=False, choices=['des', 'aes']),
|
||||
authkey=dict(required=False),
|
||||
privkey=dict(required=False),
|
||||
removeplaceholder=dict(required=False)),
|
||||
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 pysnmp module (check docs)')
|
||||
|
||||
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 False:
|
||||
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)
|
||||
|
||||
Tree = lambda: 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.ifMtu 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 not current_interface 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()
|
Loading…
Add table
Add a link
Reference in a new issue