mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-27 23:21:22 -07:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
0
plugins/lookup/__init__.py
Normal file
0
plugins/lookup/__init__.py
Normal file
127
plugins/lookup/avi.py
Normal file
127
plugins/lookup/avi.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
# python 3 headers, required if submitting to Ansible
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: avi
|
||||
author: Sandeep Bandi <sandeepb@avinetworks.com>
|
||||
short_description: Look up ``Avi`` objects.
|
||||
description:
|
||||
- Given an object_type, fetch all the objects of that type or fetch
|
||||
the specific object that matches the name/uuid given via options.
|
||||
- For single object lookup. If you want the output to be a list, you may
|
||||
want to pass option wantlist=True to the plugin.
|
||||
|
||||
options:
|
||||
obj_type:
|
||||
description:
|
||||
- type of object to query
|
||||
required: True
|
||||
obj_name:
|
||||
description:
|
||||
- name of the object to query
|
||||
obj_uuid:
|
||||
description:
|
||||
- UUID of the object to query
|
||||
extends_documentation_fragment:
|
||||
- community.general.avi
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
# Lookup query for all the objects of a specific type.
|
||||
- debug: msg="{{ lookup('avi', avi_credentials=avi_credentials, obj_type='virtualservice') }}"
|
||||
# Lookup query for an object with the given name and type.
|
||||
- debug: msg="{{ lookup('avi', avi_credentials=avi_credentials, obj_name='vs1', obj_type='virtualservice', wantlist=True) }}"
|
||||
# Lookup query for an object with the given UUID and type.
|
||||
- debug: msg="{{ lookup('avi', obj_uuid='virtualservice-5c0e183a-690a-45d8-8d6f-88c30a52550d', obj_type='virtualservice') }}"
|
||||
# We can replace lookup with query function to always the get the output as list.
|
||||
# This is helpful for looping.
|
||||
- debug: msg="{{ query('avi', obj_uuid='virtualservice-5c0e183a-690a-45d8-8d6f-88c30a52550d', obj_type='virtualservice') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- One ore more objects returned from ``Avi`` API.
|
||||
type: list
|
||||
elements: dictionary
|
||||
"""
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible_collections.community.general.plugins.module_utils.network.avi.avi_api import (ApiSession,
|
||||
AviCredentials,
|
||||
AviServerError,
|
||||
ObjectNotFound,
|
||||
APIError)
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
def _api(avi_session, path, **kwargs):
|
||||
'''
|
||||
Generic function to handle both /<obj_type>/<obj_uuid> and /<obj_type>
|
||||
API resource endpoints.
|
||||
'''
|
||||
rsp = []
|
||||
try:
|
||||
rsp_data = avi_session.get(path, **kwargs).json()
|
||||
if 'results' in rsp_data:
|
||||
rsp = rsp_data['results']
|
||||
else:
|
||||
rsp.append(rsp_data)
|
||||
except ObjectNotFound as e:
|
||||
display.warning('Resource not found. Please check obj_name/'
|
||||
'obj_uuid/obj_type are spelled correctly.')
|
||||
display.v(to_native(e))
|
||||
except (AviServerError, APIError) as e:
|
||||
raise AnsibleError(to_native(e))
|
||||
except Exception as e:
|
||||
# Generic excption handling for connection failures
|
||||
raise AnsibleError('Unable to communicate with controller'
|
||||
'due to error: %s' % to_native(e))
|
||||
|
||||
return rsp
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, avi_credentials=None, **kwargs):
|
||||
|
||||
api_creds = AviCredentials(**avi_credentials)
|
||||
# Create the session using avi_credentials
|
||||
try:
|
||||
avi = ApiSession(avi_credentials=api_creds)
|
||||
except Exception as e:
|
||||
raise AnsibleError(to_native(e))
|
||||
|
||||
# Return an empty list if the object is not found
|
||||
rsp = []
|
||||
try:
|
||||
path = kwargs.pop('obj_type')
|
||||
except KeyError:
|
||||
raise AnsibleError("Please pass the obj_type for lookup")
|
||||
|
||||
if kwargs.get('obj_name', None):
|
||||
name = kwargs.pop('obj_name')
|
||||
try:
|
||||
display.v("Fetching obj: %s of type: %s" % (name, path))
|
||||
rsp_data = avi.get_object_by_name(path, name, **kwargs)
|
||||
if rsp_data:
|
||||
# Append the return data only if it is not None. i.e object
|
||||
# with specified name is present
|
||||
rsp.append(rsp_data)
|
||||
except AviServerError as e:
|
||||
raise AnsibleError(to_native(e))
|
||||
elif kwargs.get('obj_uuid', None):
|
||||
obj_uuid = kwargs.pop('obj_uuid')
|
||||
obj_path = "%s/%s" % (path, obj_uuid)
|
||||
display.v("Fetching obj: %s of type: %s" % (obj_uuid, path))
|
||||
rsp = _api(avi, obj_path, **kwargs)
|
||||
else:
|
||||
display.v("Fetching all objects of type: %s" % path)
|
||||
rsp = _api(avi, path, **kwargs)
|
||||
|
||||
return rsp
|
73
plugins/lookup/cartesian.py
Normal file
73
plugins/lookup/cartesian.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
# (c) 2013, Bradley Young <young.bradley@gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: cartesian
|
||||
short_description: returns the cartesian product of lists
|
||||
description:
|
||||
- Takes the input lists and returns a list that represents the product of the input lists.
|
||||
- It is clearer with an example, it turns [1, 2, 3], [a, b] into [1, a], [1, b], [2, a], [2, b], [3, a], [3, b].
|
||||
You can see the exact syntax in the examples section.
|
||||
options:
|
||||
_raw:
|
||||
description:
|
||||
- a set of lists
|
||||
required: True
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Example of the change in the description
|
||||
debug: msg="{{ [1,2,3]|lookup('cartesian', [a, b])}}"
|
||||
|
||||
- name: loops over the cartesian product of the supplied lists
|
||||
debug: msg="{{item}}"
|
||||
with_cartesian:
|
||||
- "{{list1}}"
|
||||
- "{{list2}}"
|
||||
- [1,2,3,4,5,6]
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- list of lists composed of elements of the input lists
|
||||
type: lists
|
||||
"""
|
||||
|
||||
from itertools import product
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Create the cartesian product of lists
|
||||
"""
|
||||
|
||||
def _lookup_variables(self, terms):
|
||||
"""
|
||||
Turn this:
|
||||
terms == ["1,2,3", "a,b"]
|
||||
into this:
|
||||
terms == [[1,2,3], [a, b]]
|
||||
"""
|
||||
results = []
|
||||
for x in terms:
|
||||
intermediate = listify_lookup_plugin_terms(x, templar=self._templar, loader=self._loader)
|
||||
results.append(intermediate)
|
||||
return results
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
terms = self._lookup_variables(terms)
|
||||
|
||||
my_list = terms[:]
|
||||
if len(my_list) == 0:
|
||||
raise AnsibleError("with_cartesian requires at least one element in each list")
|
||||
|
||||
return [self._flatten(x) for x in product(*my_list)]
|
101
plugins/lookup/chef_databag.py
Normal file
101
plugins/lookup/chef_databag.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# (c) 2016, Josh Bradley <jbradley(at)digitalocean.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: chef_databag
|
||||
short_description: fetches data from a Chef Databag
|
||||
description:
|
||||
- "This is a lookup plugin to provide access to chef data bags using the pychef package.
|
||||
It interfaces with the chef server api using the same methods to find a knife or chef-client config file to load parameters from,
|
||||
starting from either the given base path or the current working directory.
|
||||
The lookup order mirrors the one from Chef, all folders in the base path are walked back looking for the following configuration
|
||||
file in order : .chef/knife.rb, ~/.chef/knife.rb, /etc/chef/client.rb"
|
||||
requirements:
|
||||
- "pychef (python library https://pychef.readthedocs.io `pip install pychef`)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the databag
|
||||
required: True
|
||||
item:
|
||||
description:
|
||||
- Item to fetch
|
||||
required: True
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- debug:
|
||||
msg: "{{ lookup('chef_databag', 'name=data_bag_name item=data_bag_item') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- The value from the databag
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
|
||||
try:
|
||||
import chef
|
||||
HAS_CHEF = True
|
||||
except ImportError as missing_module:
|
||||
HAS_CHEF = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Chef data bag lookup module
|
||||
"""
|
||||
def __init__(self, loader=None, templar=None, **kwargs):
|
||||
|
||||
super(LookupModule, self).__init__(loader, templar, **kwargs)
|
||||
|
||||
# setup vars for data bag name and data bag item
|
||||
self.name = None
|
||||
self.item = None
|
||||
|
||||
def parse_kv_args(self, args):
|
||||
"""
|
||||
parse key-value style arguments
|
||||
"""
|
||||
|
||||
for arg in ["name", "item"]:
|
||||
try:
|
||||
arg_raw = args.pop(arg, None)
|
||||
if arg_raw is None:
|
||||
continue
|
||||
parsed = str(arg_raw)
|
||||
setattr(self, arg, parsed)
|
||||
except ValueError:
|
||||
raise AnsibleError(
|
||||
"can't parse arg {0}={1} as string".format(arg, arg_raw)
|
||||
)
|
||||
if args:
|
||||
raise AnsibleError(
|
||||
"unrecognized arguments to with_sequence: %r" % args.keys()
|
||||
)
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
# Ensure pychef has been loaded
|
||||
if not HAS_CHEF:
|
||||
raise AnsibleError('PyChef needed for lookup plugin, try `pip install pychef`')
|
||||
|
||||
for term in terms:
|
||||
self.parse_kv_args(parse_kv(term))
|
||||
|
||||
api_object = chef.autoconfigure()
|
||||
|
||||
if not isinstance(api_object, chef.api.ChefAPI):
|
||||
raise AnsibleError('Unable to connect to Chef Server API.')
|
||||
|
||||
data_bag_object = chef.DataBag(self.name)
|
||||
|
||||
data_bag_item = data_bag_object[self.item]
|
||||
|
||||
return [dict(data_bag_item)]
|
163
plugins/lookup/conjur_variable.py
Normal file
163
plugins/lookup/conjur_variable.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
# (c) 2018, Jason Vanderhoof <jason.vanderhoof@cyberark.com>, Oren Ben Meir <oren.benmeir@cyberark.com>
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: conjur_variable
|
||||
short_description: Fetch credentials from CyberArk Conjur.
|
||||
description:
|
||||
- "Retrieves credentials from Conjur using the controlling host's Conjur identity. Conjur info: U(https://www.conjur.org/)."
|
||||
requirements:
|
||||
- 'The controlling host running Ansible has a Conjur identity.
|
||||
(More: U(https://docs.conjur.org/Latest/en/Content/Get%20Started/key_concepts/machine_identity.html))'
|
||||
options:
|
||||
_term:
|
||||
description: Variable path
|
||||
required: True
|
||||
identity_file:
|
||||
description: Path to the Conjur identity file. The identity file follows the netrc file format convention.
|
||||
type: path
|
||||
default: /etc/conjur.identity
|
||||
required: False
|
||||
ini:
|
||||
- section: conjur,
|
||||
key: identity_file_path
|
||||
env:
|
||||
- name: CONJUR_IDENTITY_FILE
|
||||
config_file:
|
||||
description: Path to the Conjur configuration file. The configuration file is a YAML file.
|
||||
type: path
|
||||
default: /etc/conjur.conf
|
||||
required: False
|
||||
ini:
|
||||
- section: conjur,
|
||||
key: config_file_path
|
||||
env:
|
||||
- name: CONJUR_CONFIG_FILE
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- debug:
|
||||
msg: "{{ lookup('conjur_variable', '/path/to/secret') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- Value stored in Conjur.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from base64 import b64encode
|
||||
from netrc import netrc
|
||||
from os import environ
|
||||
from time import time
|
||||
from ansible.module_utils.six.moves.urllib.parse import quote_plus
|
||||
import yaml
|
||||
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
# Load configuration and return as dictionary if file is present on file system
|
||||
def _load_conf_from_file(conf_path):
|
||||
display.vvv('conf file: {0}'.format(conf_path))
|
||||
|
||||
if not os.path.exists(conf_path):
|
||||
raise AnsibleError('Conjur configuration file `{0}` was not found on the controlling host'
|
||||
.format(conf_path))
|
||||
|
||||
display.vvvv('Loading configuration from: {0}'.format(conf_path))
|
||||
with open(conf_path) as f:
|
||||
config = yaml.safe_load(f.read())
|
||||
if 'account' not in config or 'appliance_url' not in config:
|
||||
raise AnsibleError('{0} on the controlling host must contain an `account` and `appliance_url` entry'
|
||||
.format(conf_path))
|
||||
return config
|
||||
|
||||
|
||||
# Load identity and return as dictionary if file is present on file system
|
||||
def _load_identity_from_file(identity_path, appliance_url):
|
||||
display.vvvv('identity file: {0}'.format(identity_path))
|
||||
|
||||
if not os.path.exists(identity_path):
|
||||
raise AnsibleError('Conjur identity file `{0}` was not found on the controlling host'
|
||||
.format(identity_path))
|
||||
|
||||
display.vvvv('Loading identity from: {0} for {1}'.format(identity_path, appliance_url))
|
||||
|
||||
conjur_authn_url = '{0}/authn'.format(appliance_url)
|
||||
identity = netrc(identity_path)
|
||||
|
||||
if identity.authenticators(conjur_authn_url) is None:
|
||||
raise AnsibleError('The netrc file on the controlling host does not contain an entry for: {0}'
|
||||
.format(conjur_authn_url))
|
||||
|
||||
id, account, api_key = identity.authenticators(conjur_authn_url)
|
||||
if not id or not api_key:
|
||||
raise AnsibleError('{0} on the controlling host must contain a `login` and `password` entry for {1}'
|
||||
.format(identity_path, appliance_url))
|
||||
|
||||
return {'id': id, 'api_key': api_key}
|
||||
|
||||
|
||||
# Use credentials to retrieve temporary authorization token
|
||||
def _fetch_conjur_token(conjur_url, account, username, api_key):
|
||||
conjur_url = '{0}/authn/{1}/{2}/authenticate'.format(conjur_url, account, username)
|
||||
display.vvvv('Authentication request to Conjur at: {0}, with user: {1}'.format(conjur_url, username))
|
||||
|
||||
response = open_url(conjur_url, data=api_key, method='POST')
|
||||
code = response.getcode()
|
||||
if code != 200:
|
||||
raise AnsibleError('Failed to authenticate as \'{0}\' (got {1} response)'
|
||||
.format(username, code))
|
||||
|
||||
return response.read()
|
||||
|
||||
|
||||
# Retrieve Conjur variable using the temporary token
|
||||
def _fetch_conjur_variable(conjur_variable, token, conjur_url, account):
|
||||
token = b64encode(token)
|
||||
headers = {'Authorization': 'Token token="{0}"'.format(token)}
|
||||
display.vvvv('Header: {0}'.format(headers))
|
||||
|
||||
url = '{0}/secrets/{1}/variable/{2}'.format(conjur_url, account, quote_plus(conjur_variable))
|
||||
display.vvvv('Conjur Variable URL: {0}'.format(url))
|
||||
|
||||
response = open_url(url, headers=headers, method='GET')
|
||||
|
||||
if response.getcode() == 200:
|
||||
display.vvvv('Conjur variable {0} was successfully retrieved'.format(conjur_variable))
|
||||
return [response.read()]
|
||||
if response.getcode() == 401:
|
||||
raise AnsibleError('Conjur request has invalid authorization credentials')
|
||||
if response.getcode() == 403:
|
||||
raise AnsibleError('The controlling host\'s Conjur identity does not have authorization to retrieve {0}'
|
||||
.format(conjur_variable))
|
||||
if response.getcode() == 404:
|
||||
raise AnsibleError('The variable {0} does not exist'.format(conjur_variable))
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
conf_file = self.get_option('config_file')
|
||||
conf = _load_conf_from_file(conf_file)
|
||||
|
||||
identity_file = self.get_option('identity_file')
|
||||
identity = _load_identity_from_file(identity_file, conf['appliance_url'])
|
||||
|
||||
token = _fetch_conjur_token(conf['appliance_url'], conf['account'], identity['id'], identity['api_key'])
|
||||
return _fetch_conjur_variable(terms[0], token, conf['appliance_url'], conf['account'])
|
181
plugins/lookup/consul_kv.py
Normal file
181
plugins/lookup/consul_kv.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: consul_kv
|
||||
short_description: Fetch metadata from a Consul key value store.
|
||||
description:
|
||||
- Lookup metadata for a playbook from the key value store in a Consul cluster.
|
||||
Values can be easily set in the kv store with simple rest commands
|
||||
- C(curl -X PUT -d 'some-value' http://localhost:8500/v1/kv/ansible/somedata)
|
||||
requirements:
|
||||
- 'python-consul python library U(https://python-consul.readthedocs.io/en/latest/#installation)'
|
||||
options:
|
||||
_raw:
|
||||
description: List of key(s) to retrieve.
|
||||
type: list
|
||||
required: True
|
||||
recurse:
|
||||
type: boolean
|
||||
description: If true, will retrieve all the values that have the given key as prefix.
|
||||
default: False
|
||||
index:
|
||||
description:
|
||||
- If the key has a value with the specified index then this is returned allowing access to historical values.
|
||||
datacenter:
|
||||
description:
|
||||
- Retrieve the key from a consul datatacenter other than the default for the consul host.
|
||||
token:
|
||||
description: The acl token to allow access to restricted values.
|
||||
host:
|
||||
default: localhost
|
||||
description:
|
||||
- The target to connect to, must be a resolvable address.
|
||||
Will be determined from C(ANSIBLE_CONSUL_URL) if that is set.
|
||||
- "C(ANSIBLE_CONSUL_URL) should look like this: C(https://my.consul.server:8500)"
|
||||
env:
|
||||
- name: ANSIBLE_CONSUL_URL
|
||||
ini:
|
||||
- section: lookup_consul
|
||||
key: host
|
||||
port:
|
||||
description:
|
||||
- The port of the target host to connect to.
|
||||
- If you use C(ANSIBLE_CONSUL_URL) this value will be used from there.
|
||||
default: 8500
|
||||
scheme:
|
||||
default: http
|
||||
description:
|
||||
- Whether to use http or https.
|
||||
- If you use C(ANSIBLE_CONSUL_URL) this value will be used from there.
|
||||
validate_certs:
|
||||
default: True
|
||||
description: Whether to verify the ssl connection or not.
|
||||
env:
|
||||
- name: ANSIBLE_CONSUL_VALIDATE_CERTS
|
||||
ini:
|
||||
- section: lookup_consul
|
||||
key: validate_certs
|
||||
client_cert:
|
||||
description: The client cert to verify the ssl connection.
|
||||
env:
|
||||
- name: ANSIBLE_CONSUL_CLIENT_CERT
|
||||
ini:
|
||||
- section: lookup_consul
|
||||
key: client_cert
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- debug:
|
||||
msg: 'key contains {{item}}'
|
||||
with_consul_kv:
|
||||
- 'key/to/retrieve'
|
||||
|
||||
- name: Parameters can be provided after the key be more specific about what to retrieve
|
||||
debug:
|
||||
msg: 'key contains {{item}}'
|
||||
with_consul_kv:
|
||||
- 'key/to recurse=true token=E6C060A9-26FB-407A-B83E-12DDAFCB4D98'
|
||||
|
||||
- name: retrieving a KV from a remote cluster on non default port
|
||||
debug:
|
||||
msg: "{{ lookup('consul_kv', 'my/key', host='10.10.10.10', port='2000') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- Value(s) stored in consul.
|
||||
"""
|
||||
|
||||
import os
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
try:
|
||||
import consul
|
||||
|
||||
HAS_CONSUL = True
|
||||
except ImportError as e:
|
||||
HAS_CONSUL = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if not HAS_CONSUL:
|
||||
raise AnsibleError(
|
||||
'python-consul is required for consul_kv lookup. see http://python-consul.readthedocs.org/en/latest/#installation')
|
||||
|
||||
values = []
|
||||
try:
|
||||
for term in terms:
|
||||
params = self.parse_params(term)
|
||||
try:
|
||||
url = os.environ['ANSIBLE_CONSUL_URL']
|
||||
validate_certs = os.environ['ANSIBLE_CONSUL_VALIDATE_CERTS'] or True
|
||||
client_cert = os.environ['ANSIBLE_CONSUL_CLIENT_CERT'] or None
|
||||
u = urlparse(url)
|
||||
consul_api = consul.Consul(host=u.hostname, port=u.port, scheme=u.scheme, verify=validate_certs,
|
||||
cert=client_cert)
|
||||
except KeyError:
|
||||
port = kwargs.get('port', '8500')
|
||||
host = kwargs.get('host', 'localhost')
|
||||
scheme = kwargs.get('scheme', 'http')
|
||||
validate_certs = kwargs.get('validate_certs', True)
|
||||
client_cert = kwargs.get('client_cert', None)
|
||||
consul_api = consul.Consul(host=host, port=port, scheme=scheme, verify=validate_certs,
|
||||
cert=client_cert)
|
||||
|
||||
results = consul_api.kv.get(params['key'],
|
||||
token=params['token'],
|
||||
index=params['index'],
|
||||
recurse=params['recurse'],
|
||||
dc=params['datacenter'])
|
||||
if results[1]:
|
||||
# responds with a single or list of result maps
|
||||
if isinstance(results[1], list):
|
||||
for r in results[1]:
|
||||
values.append(to_text(r['Value']))
|
||||
else:
|
||||
values.append(to_text(results[1]['Value']))
|
||||
except Exception as e:
|
||||
raise AnsibleError(
|
||||
"Error locating '%s' in kv store. Error was %s" % (term, e))
|
||||
|
||||
return values
|
||||
|
||||
def parse_params(self, term):
|
||||
params = term.split(' ')
|
||||
|
||||
paramvals = {
|
||||
'key': params[0],
|
||||
'token': None,
|
||||
'recurse': False,
|
||||
'index': None,
|
||||
'datacenter': None
|
||||
}
|
||||
|
||||
# parameters specified?
|
||||
try:
|
||||
for param in params[1:]:
|
||||
if param and len(param) > 0:
|
||||
name, value = param.split('=')
|
||||
if name not in paramvals:
|
||||
raise AnsibleAssertionError("%s not a valid consul lookup parameter" % name)
|
||||
paramvals[name] = value
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
|
||||
return paramvals
|
118
plugins/lookup/credstash.py
Normal file
118
plugins/lookup/credstash.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
# (c) 2015, Ensighten <infra@ensighten.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: credstash
|
||||
short_description: retrieve secrets from Credstash on AWS
|
||||
requirements:
|
||||
- credstash (python library)
|
||||
description:
|
||||
- "Credstash is a small utility for managing secrets using AWS's KMS and DynamoDB: https://github.com/fugue/credstash"
|
||||
options:
|
||||
_terms:
|
||||
description: term or list of terms to lookup in the credit store
|
||||
type: list
|
||||
required: True
|
||||
table:
|
||||
description: name of the credstash table to query
|
||||
default: 'credential-store'
|
||||
required: True
|
||||
version:
|
||||
description: Credstash version
|
||||
region:
|
||||
description: AWS region
|
||||
profile_name:
|
||||
description: AWS profile to use for authentication
|
||||
env:
|
||||
- name: AWS_PROFILE
|
||||
aws_access_key_id:
|
||||
description: AWS access key ID
|
||||
env:
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
aws_secret_access_key:
|
||||
description: AWS access key
|
||||
env:
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
aws_session_token:
|
||||
description: AWS session token
|
||||
env:
|
||||
- name: AWS_SESSION_TOKEN
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: first use credstash to store your secrets
|
||||
shell: credstash put my-github-password secure123
|
||||
|
||||
- name: "Test credstash lookup plugin -- get my github password"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'my-github-password') }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get my other password from us-west-1"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'my-other-password', region='us-west-1') }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the company's github password"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'company-github-password', table='company-passwords') }}"
|
||||
|
||||
- name: Example play using the 'context' feature
|
||||
hosts: localhost
|
||||
vars:
|
||||
context:
|
||||
app: my_app
|
||||
environment: production
|
||||
tasks:
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the password with a context passed as a variable"
|
||||
debug: msg="{{ lookup('credstash', 'some-password', context=context) }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the password with a context defined here"
|
||||
debug: msg="{{ lookup('credstash', 'some-password', context=dict(app='my_app', environment='production')) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- value(s) stored in Credstash
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
CREDSTASH_INSTALLED = False
|
||||
|
||||
try:
|
||||
import credstash
|
||||
CREDSTASH_INSTALLED = True
|
||||
except ImportError:
|
||||
CREDSTASH_INSTALLED = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not CREDSTASH_INSTALLED:
|
||||
raise AnsibleError('The credstash lookup plugin requires credstash to be installed.')
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
try:
|
||||
version = kwargs.pop('version', '')
|
||||
region = kwargs.pop('region', None)
|
||||
table = kwargs.pop('table', 'credential-store')
|
||||
profile_name = kwargs.pop('profile_name', os.getenv('AWS_PROFILE', None))
|
||||
aws_access_key_id = kwargs.pop('aws_access_key_id', os.getenv('AWS_ACCESS_KEY_ID', None))
|
||||
aws_secret_access_key = kwargs.pop('aws_secret_access_key', os.getenv('AWS_SECRET_ACCESS_KEY', None))
|
||||
aws_session_token = kwargs.pop('aws_session_token', os.getenv('AWS_SESSION_TOKEN', None))
|
||||
kwargs_pass = {'profile_name': profile_name, 'aws_access_key_id': aws_access_key_id,
|
||||
'aws_secret_access_key': aws_secret_access_key, 'aws_session_token': aws_session_token}
|
||||
val = credstash.getSecret(term, version, region, table, context=kwargs, **kwargs_pass)
|
||||
except credstash.ItemNotFound:
|
||||
raise AnsibleError('Key {0} not found'.format(term))
|
||||
except Exception as e:
|
||||
raise AnsibleError('Encountered exception while fetching {0}: {1}'.format(term, e))
|
||||
ret.append(val)
|
||||
|
||||
return ret
|
179
plugins/lookup/cyberarkpassword.py
Normal file
179
plugins/lookup/cyberarkpassword.py
Normal file
|
@ -0,0 +1,179 @@
|
|||
# (c) 2017, Edward Nunez <edward.nunez@cyberark.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: cyberarkpassword
|
||||
short_description: get secrets from CyberArk AIM
|
||||
requirements:
|
||||
- CyberArk AIM tool installed
|
||||
description:
|
||||
- Get secrets from CyberArk AIM.
|
||||
options :
|
||||
_command:
|
||||
description: Cyberark CLI utility.
|
||||
env:
|
||||
- name: AIM_CLIPASSWORDSDK_CMD
|
||||
default: '/opt/CARKaim/sdk/clipasswordsdk'
|
||||
appid:
|
||||
description: Defines the unique ID of the application that is issuing the password request.
|
||||
required: True
|
||||
query:
|
||||
description: Describes the filter criteria for the password retrieval.
|
||||
required: True
|
||||
output:
|
||||
description:
|
||||
- Specifies the desired output fields separated by commas.
|
||||
- "They could be: Password, PassProps.<property>, PasswordChangeInProcess"
|
||||
default: 'password'
|
||||
_extra:
|
||||
description: for extra_parms values please check parameters for clipasswordsdk in CyberArk's "Credential Provider and ASCP Implementation Guide"
|
||||
note:
|
||||
- For Ansible on windows, please change the -parameters (-p, -d, and -o) to /parameters (/p, /d, and /o) and change the location of CLIPasswordSDK.exe
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: passing options to the lookup
|
||||
debug: msg={{ lookup("cyberarkpassword", cyquery)}}
|
||||
vars:
|
||||
cyquery:
|
||||
appid: "app_ansible"
|
||||
query: "safe=CyberArk_Passwords;folder=root;object=AdminPass"
|
||||
output: "Password,PassProps.UserName,PassProps.Address,PasswordChangeInProcess"
|
||||
|
||||
|
||||
- name: used in a loop
|
||||
debug: msg={{item}}
|
||||
with_cyberarkpassword:
|
||||
appid: 'app_ansible'
|
||||
query: 'safe=CyberArk_Passwords;folder=root;object=AdminPass'
|
||||
output: 'Password,PassProps.UserName,PassProps.Address,PasswordChangeInProcess'
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
password:
|
||||
description:
|
||||
- The actual value stored
|
||||
passprops:
|
||||
description: properties assigned to the entry
|
||||
type: dictionary
|
||||
passwordchangeinprocess:
|
||||
description: did the password change?
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
from subprocess import Popen
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.module_utils._text import to_bytes, to_text, to_native
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
CLIPASSWORDSDK_CMD = os.getenv('AIM_CLIPASSWORDSDK_CMD', '/opt/CARKaim/sdk/clipasswordsdk')
|
||||
|
||||
|
||||
class CyberarkPassword:
|
||||
|
||||
def __init__(self, appid=None, query=None, output=None, **kwargs):
|
||||
|
||||
self.appid = appid
|
||||
self.query = query
|
||||
self.output = output
|
||||
|
||||
# Support for Generic parameters to be able to specify
|
||||
# FailRequestOnPasswordChange, Queryformat, Reason, etc.
|
||||
self.extra_parms = []
|
||||
for key, value in kwargs.items():
|
||||
self.extra_parms.append('-p')
|
||||
self.extra_parms.append("%s=%s" % (key, value))
|
||||
|
||||
if self.appid is None:
|
||||
raise AnsibleError("CyberArk Error: No Application ID specified")
|
||||
if self.query is None:
|
||||
raise AnsibleError("CyberArk Error: No Vault query specified")
|
||||
|
||||
if self.output is None:
|
||||
# If no output is specified, return at least the password
|
||||
self.output = "password"
|
||||
else:
|
||||
# To avoid reference issues/confusion to values, all
|
||||
# output 'keys' will be in lowercase.
|
||||
self.output = self.output.lower()
|
||||
|
||||
self.b_delimiter = b"@#@" # Known delimiter to split output results
|
||||
|
||||
def get(self):
|
||||
|
||||
result_dict = {}
|
||||
|
||||
try:
|
||||
all_parms = [
|
||||
CLIPASSWORDSDK_CMD,
|
||||
'GetPassword',
|
||||
'-p', 'AppDescs.AppID=%s' % self.appid,
|
||||
'-p', 'Query=%s' % self.query,
|
||||
'-o', self.output,
|
||||
'-d', self.b_delimiter]
|
||||
all_parms.extend(self.extra_parms)
|
||||
|
||||
b_credential = b""
|
||||
b_all_params = [to_bytes(v) for v in all_parms]
|
||||
tmp_output, tmp_error = Popen(b_all_params, stdout=PIPE, stderr=PIPE, stdin=PIPE).communicate()
|
||||
|
||||
if tmp_output:
|
||||
b_credential = to_bytes(tmp_output)
|
||||
|
||||
if tmp_error:
|
||||
raise AnsibleError("ERROR => %s " % (tmp_error))
|
||||
|
||||
if b_credential and b_credential.endswith(b'\n'):
|
||||
b_credential = b_credential[:-1]
|
||||
|
||||
output_names = self.output.split(",")
|
||||
output_values = b_credential.split(self.b_delimiter)
|
||||
|
||||
for i in range(len(output_names)):
|
||||
if output_names[i].startswith("passprops."):
|
||||
if "passprops" not in result_dict:
|
||||
result_dict["passprops"] = {}
|
||||
output_prop_name = output_names[i][10:]
|
||||
result_dict["passprops"][output_prop_name] = to_native(output_values[i])
|
||||
else:
|
||||
result_dict[output_names[i]] = to_native(output_values[i])
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise AnsibleError(e.output)
|
||||
except OSError as e:
|
||||
raise AnsibleError("ERROR - AIM not installed or clipasswordsdk not in standard location. ERROR=(%s) => %s " % (to_text(e.errno), e.strerror))
|
||||
|
||||
return [result_dict]
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
"""
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
display.vvvv("%s" % terms)
|
||||
if isinstance(terms, list):
|
||||
return_values = []
|
||||
for term in terms:
|
||||
display.vvvv("Term: %s" % term)
|
||||
cyberark_conn = CyberarkPassword(**term)
|
||||
return_values.append(cyberark_conn.get())
|
||||
return return_values
|
||||
else:
|
||||
cyberark_conn = CyberarkPassword(**terms)
|
||||
result = cyberark_conn.get()
|
||||
return result
|
301
plugins/lookup/dig.py
Normal file
301
plugins/lookup/dig.py
Normal file
|
@ -0,0 +1,301 @@
|
|||
# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: dig
|
||||
author: Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com>
|
||||
short_description: query DNS using the dnspython library
|
||||
requirements:
|
||||
- dnspython (python library, http://www.dnspython.org/)
|
||||
description:
|
||||
- The dig lookup runs queries against DNS servers to retrieve DNS records for a specific name (FQDN - fully qualified domain name).
|
||||
It is possible to lookup any DNS record in this manner.
|
||||
- There is a couple of different syntaxes that can be used to specify what record should be retrieved, and for which name.
|
||||
It is also possible to explicitly specify the DNS server(s) to use for lookups.
|
||||
- In its simplest form, the dig lookup plugin can be used to retrieve an IPv4 address (DNS A record) associated with FQDN
|
||||
- In addition to (default) A record, it is also possible to specify a different record type that should be queried.
|
||||
This can be done by either passing-in additional parameter of format qtype=TYPE to the dig lookup, or by appending /TYPE to the FQDN being queried.
|
||||
- If multiple values are associated with the requested record, the results will be returned as a comma-separated list.
|
||||
In such cases you may want to pass option wantlist=True to the plugin, which will result in the record values being returned as a list
|
||||
over which you can iterate later on.
|
||||
- By default, the lookup will rely on system-wide configured DNS servers for performing the query.
|
||||
It is also possible to explicitly specify DNS servers to query using the @DNS_SERVER_1,DNS_SERVER_2,...,DNS_SERVER_N notation.
|
||||
This needs to be passed-in as an additional parameter to the lookup
|
||||
options:
|
||||
_terms:
|
||||
description: domain(s) to query
|
||||
qtype:
|
||||
description: record type to query
|
||||
default: 'A'
|
||||
choices: [A, ALL, AAAA, CNAME, DNAME, DLV, DNSKEY, DS, HINFO, LOC, MX, NAPTR, NS, NSEC3PARAM, PTR, RP, RRSIG, SOA, SPF, SRV, SSHFP, TLSA, TXT]
|
||||
flat:
|
||||
description: If 0 each record is returned as a dictionary, otherwise a string
|
||||
default: 1
|
||||
notes:
|
||||
- ALL is not a record per-se, merely the listed fields are available for any record results you retrieve in the form of a dictionary.
|
||||
- While the 'dig' lookup plugin supports anything which dnspython supports out of the box, only a subset can be converted into a dictionary.
|
||||
- If you need to obtain the AAAA record (IPv6 address), you must specify the record type explicitly.
|
||||
Syntax for specifying the record type is shown in the examples below.
|
||||
- The trailing dot in most of the examples listed is purely optional, but is specified for completeness/correctness sake.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Simple A record (IPV4 address) lookup for example.com
|
||||
debug: msg="{{ lookup('dig', 'example.com.')}}"
|
||||
|
||||
- name: "The TXT record for example.org."
|
||||
debug: msg="{{ lookup('dig', 'example.org.', 'qtype=TXT') }}"
|
||||
|
||||
- name: "The TXT record for example.org, alternative syntax."
|
||||
debug: msg="{{ lookup('dig', 'example.org./TXT') }}"
|
||||
|
||||
- name: use in a loop
|
||||
debug: msg="MX record for gmail.com {{ item }}"
|
||||
with_items: "{{ lookup('dig', 'gmail.com./MX', wantlist=True) }}"
|
||||
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '192.0.2.5/PTR') }}"
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa./PTR') }}"
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa.', 'qtype=PTR') }}"
|
||||
- debug: msg="Querying 198.51.100.23 for IPv4 address for example.com. produces {{ lookup('dig', 'example.com', '@198.51.100.23') }}"
|
||||
|
||||
- debug: msg="XMPP service for gmail.com. is available at {{ item.target }} on port {{ item.port }}"
|
||||
with_items: "{{ lookup('dig', '_xmpp-server._tcp.gmail.com./SRV', 'flat=0', wantlist=True) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- list of composed strings or dictonaries with key and value
|
||||
If a dictionary, fields shows the keys returned depending on query type
|
||||
fields:
|
||||
ALL: owner, ttl, type
|
||||
A: address
|
||||
AAAA: address
|
||||
CNAME: target
|
||||
DNAME: target
|
||||
DLV: algorithm, digest_type, key_tag, digest
|
||||
DNSKEY: flags, algorithm, protocol, key
|
||||
DS: algorithm, digest_type, key_tag, digest
|
||||
HINFO: cpu, os
|
||||
LOC: latitude, longitude, altitude, size, horizontal_precision, vertical_precision
|
||||
MX: preference, exchange
|
||||
NAPTR: order, preference, flags, service, regexp, replacement
|
||||
NS: target
|
||||
NSEC3PARAM: algorithm, flags, iterations, salt
|
||||
PTR: target
|
||||
RP: mbox, txt
|
||||
SOA: mname, rname, serial, refresh, retry, expire, minimum
|
||||
SPF: strings
|
||||
SRV: priority, weight, port, target
|
||||
SSHFP: algorithm, fp_type, fingerprint
|
||||
TLSA: usage, selector, mtype, cert
|
||||
TXT: strings
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_native
|
||||
import socket
|
||||
|
||||
try:
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.resolver
|
||||
import dns.reversename
|
||||
import dns.rdataclass
|
||||
from dns.rdatatype import (A, AAAA, CNAME, DLV, DNAME, DNSKEY, DS, HINFO, LOC,
|
||||
MX, NAPTR, NS, NSEC3PARAM, PTR, RP, SOA, SPF, SRV, SSHFP, TLSA, TXT)
|
||||
HAVE_DNS = True
|
||||
except ImportError:
|
||||
HAVE_DNS = False
|
||||
|
||||
|
||||
def make_rdata_dict(rdata):
|
||||
''' While the 'dig' lookup plugin supports anything which dnspython supports
|
||||
out of the box, the following supported_types list describes which
|
||||
DNS query types we can convert to a dict.
|
||||
|
||||
Note: adding support for RRSIG is hard work. :)
|
||||
'''
|
||||
supported_types = {
|
||||
A: ['address'],
|
||||
AAAA: ['address'],
|
||||
CNAME: ['target'],
|
||||
DNAME: ['target'],
|
||||
DLV: ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
||||
DNSKEY: ['flags', 'algorithm', 'protocol', 'key'],
|
||||
DS: ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
||||
HINFO: ['cpu', 'os'],
|
||||
LOC: ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'],
|
||||
MX: ['preference', 'exchange'],
|
||||
NAPTR: ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'],
|
||||
NS: ['target'],
|
||||
NSEC3PARAM: ['algorithm', 'flags', 'iterations', 'salt'],
|
||||
PTR: ['target'],
|
||||
RP: ['mbox', 'txt'],
|
||||
# RRSIG: ['algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'signature'],
|
||||
SOA: ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'],
|
||||
SPF: ['strings'],
|
||||
SRV: ['priority', 'weight', 'port', 'target'],
|
||||
SSHFP: ['algorithm', 'fp_type', 'fingerprint'],
|
||||
TLSA: ['usage', 'selector', 'mtype', 'cert'],
|
||||
TXT: ['strings'],
|
||||
}
|
||||
|
||||
rd = {}
|
||||
|
||||
if rdata.rdtype in supported_types:
|
||||
fields = supported_types[rdata.rdtype]
|
||||
for f in fields:
|
||||
val = rdata.__getattribute__(f)
|
||||
|
||||
if isinstance(val, dns.name.Name):
|
||||
val = dns.name.Name.to_text(val)
|
||||
|
||||
if rdata.rdtype == DLV and f == 'digest':
|
||||
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
||||
if rdata.rdtype == DS and f == 'digest':
|
||||
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
||||
if rdata.rdtype == DNSKEY and f == 'key':
|
||||
val = dns.rdata._base64ify(rdata.key).replace(' ', '')
|
||||
if rdata.rdtype == NSEC3PARAM and f == 'salt':
|
||||
val = dns.rdata._hexify(rdata.salt).replace(' ', '')
|
||||
if rdata.rdtype == SSHFP and f == 'fingerprint':
|
||||
val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '')
|
||||
if rdata.rdtype == TLSA and f == 'cert':
|
||||
val = dns.rdata._hexify(rdata.cert).replace(' ', '')
|
||||
|
||||
rd[f] = val
|
||||
|
||||
return rd
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# dig: Lookup DNS records
|
||||
#
|
||||
# --------------------------------------------------------------
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
'''
|
||||
terms contains a string with things to `dig' for. We support the
|
||||
following formats:
|
||||
example.com # A record
|
||||
example.com qtype=A # same
|
||||
example.com/TXT # specific qtype
|
||||
example.com qtype=txt # same
|
||||
192.0.2.23/PTR # reverse PTR
|
||||
^^ shortcut for 23.2.0.192.in-addr.arpa/PTR
|
||||
example.net/AAAA @nameserver # query specified server
|
||||
^^^ can be comma-sep list of names/addresses
|
||||
|
||||
... flat=0 # returns a dict; default is 1 == string
|
||||
'''
|
||||
|
||||
if HAVE_DNS is False:
|
||||
raise AnsibleError("The dig lookup requires the python 'dnspython' library and it is not installed")
|
||||
|
||||
# Create Resolver object so that we can set NS if necessary
|
||||
myres = dns.resolver.Resolver(configure=True)
|
||||
edns_size = 4096
|
||||
myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
|
||||
|
||||
domain = None
|
||||
qtype = 'A'
|
||||
flat = True
|
||||
rdclass = dns.rdataclass.from_text('IN')
|
||||
|
||||
for t in terms:
|
||||
if t.startswith('@'): # e.g. "@10.0.1.2,192.0.2.1" is ok.
|
||||
nsset = t[1:].split(',')
|
||||
for ns in nsset:
|
||||
nameservers = []
|
||||
# Check if we have a valid IP address. If so, use that, otherwise
|
||||
# try to resolve name to address using system's resolver. If that
|
||||
# fails we bail out.
|
||||
try:
|
||||
socket.inet_aton(ns)
|
||||
nameservers.append(ns)
|
||||
except Exception:
|
||||
try:
|
||||
nsaddr = dns.resolver.query(ns)[0].address
|
||||
nameservers.append(nsaddr)
|
||||
except Exception as e:
|
||||
raise AnsibleError("dns lookup NS: %s" % to_native(e))
|
||||
myres.nameservers = nameservers
|
||||
continue
|
||||
if '=' in t:
|
||||
try:
|
||||
opt, arg = t.split('=')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if opt == 'qtype':
|
||||
qtype = arg.upper()
|
||||
elif opt == 'flat':
|
||||
flat = int(arg)
|
||||
elif opt == 'class':
|
||||
try:
|
||||
rdclass = dns.rdataclass.from_text(arg)
|
||||
except Exception as e:
|
||||
raise AnsibleError("dns lookup illegal CLASS: %s" % to_native(e))
|
||||
|
||||
continue
|
||||
|
||||
if '/' in t:
|
||||
try:
|
||||
domain, qtype = t.split('/')
|
||||
except Exception:
|
||||
domain = t
|
||||
else:
|
||||
domain = t
|
||||
|
||||
# print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
|
||||
|
||||
ret = []
|
||||
|
||||
if qtype.upper() == 'PTR':
|
||||
try:
|
||||
n = dns.reversename.from_address(domain)
|
||||
domain = n.to_text()
|
||||
except dns.exception.SyntaxError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
|
||||
|
||||
try:
|
||||
answers = myres.query(domain, qtype, rdclass=rdclass)
|
||||
for rdata in answers:
|
||||
s = rdata.to_text()
|
||||
if qtype.upper() == 'TXT':
|
||||
s = s[1:-1] # Strip outside quotes on TXT rdata
|
||||
|
||||
if flat:
|
||||
ret.append(s)
|
||||
else:
|
||||
try:
|
||||
rd = make_rdata_dict(rdata)
|
||||
rd['owner'] = answers.canonical_name.to_text()
|
||||
rd['type'] = dns.rdatatype.to_text(rdata.rdtype)
|
||||
rd['ttl'] = answers.rrset.ttl
|
||||
rd['class'] = dns.rdataclass.to_text(rdata.rdclass)
|
||||
|
||||
ret.append(rd)
|
||||
except Exception as e:
|
||||
ret.append(str(e))
|
||||
|
||||
except dns.resolver.NXDOMAIN:
|
||||
ret.append('NXDOMAIN')
|
||||
except dns.resolver.NoAnswer:
|
||||
ret.append("")
|
||||
except dns.resolver.Timeout:
|
||||
ret.append('')
|
||||
except dns.exception.DNSException as e:
|
||||
raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e))
|
||||
|
||||
return ret
|
93
plugins/lookup/dnstxt.py
Normal file
93
plugins/lookup/dnstxt.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
# (c) 2012, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: dnstxt
|
||||
author: Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com>
|
||||
short_description: query a domain(s)'s DNS txt fields
|
||||
requirements:
|
||||
- dns/dns.resolver (python library)
|
||||
description:
|
||||
- Uses a python library to return the DNS TXT record for a domain.
|
||||
options:
|
||||
_terms:
|
||||
description: domain or list of domains to query TXT records from
|
||||
required: True
|
||||
type: list
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: show txt entry
|
||||
debug: msg="{{lookup('dnstxt', ['test.example.com'])}}"
|
||||
|
||||
- name: iterate over txt entries
|
||||
debug: msg="{{item}}"
|
||||
with_dnstxt:
|
||||
- 'test.example.com'
|
||||
- 'other.example.com'
|
||||
- 'last.example.com'
|
||||
|
||||
- name: iterate of a comma delimited DNS TXT entry
|
||||
debug: msg="{{item}}"
|
||||
with_dnstxt: "{{lookup('dnstxt', ['test.example.com']).split(',')}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- values returned by the DNS TXT record.
|
||||
type: list
|
||||
"""
|
||||
|
||||
HAVE_DNS = False
|
||||
try:
|
||||
import dns.resolver
|
||||
from dns.exception import DNSException
|
||||
HAVE_DNS = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
# ==============================================================
|
||||
# DNSTXT: DNS TXT records
|
||||
#
|
||||
# key=domainname
|
||||
# TODO: configurable resolver IPs
|
||||
# --------------------------------------------------------------
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if HAVE_DNS is False:
|
||||
raise AnsibleError("Can't LOOKUP(dnstxt): module dns.resolver is not installed")
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
domain = term.split()[0]
|
||||
string = []
|
||||
try:
|
||||
answers = dns.resolver.query(domain, 'TXT')
|
||||
for rdata in answers:
|
||||
s = rdata.to_text()
|
||||
string.append(s[1:-1]) # Strip outside quotes on TXT rdata
|
||||
|
||||
except dns.resolver.NXDOMAIN:
|
||||
string = 'NXDOMAIN'
|
||||
except dns.resolver.Timeout:
|
||||
string = ''
|
||||
except dns.resolver.NoAnswer:
|
||||
string = ''
|
||||
except DNSException as e:
|
||||
raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e))
|
||||
|
||||
ret.append(''.join(string))
|
||||
|
||||
return ret
|
177
plugins/lookup/etcd.py
Normal file
177
plugins/lookup/etcd.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
# (c) 2013, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (m) 2016, Mihai Moldovanu <mihaim@tfm.ro>
|
||||
# (m) 2017, Juan Manuel Parrilla <jparrill@redhat.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens)
|
||||
lookup: etcd
|
||||
short_description: get info from an etcd server
|
||||
description:
|
||||
- Retrieves data from an etcd server
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- the list of keys to lookup on the etcd server
|
||||
type: list
|
||||
elements: string
|
||||
required: True
|
||||
url:
|
||||
description:
|
||||
- Environment variable with the url for the etcd server
|
||||
default: 'http://127.0.0.1:4001'
|
||||
env:
|
||||
- name: ANSIBLE_ETCD_URL
|
||||
version:
|
||||
description:
|
||||
- Environment variable with the etcd protocol version
|
||||
default: 'v1'
|
||||
env:
|
||||
- name: ANSIBLE_ETCD_VERSION
|
||||
validate_certs:
|
||||
description:
|
||||
- toggle checking that the ssl certificates are valid, you normally only want to turn this off with self-signed certs.
|
||||
default: True
|
||||
type: boolean
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "a value from a locally running etcd"
|
||||
debug: msg={{ lookup('etcd', 'foo/bar') }}
|
||||
|
||||
- name: "values from multiple folders on a locally running etcd"
|
||||
debug: msg={{ lookup('etcd', 'foo', 'bar', 'baz') }}
|
||||
|
||||
- name: "since Ansible 2.5 you can set server options inline"
|
||||
debug: msg="{{ lookup('etcd', 'foo', version='v2', url='http://192.168.0.27:4001') }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description:
|
||||
- list of values associated with input keys
|
||||
type: list
|
||||
elements: strings
|
||||
'''
|
||||
|
||||
import json
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
# this can be made configurable, not should not use ansible.cfg
|
||||
#
|
||||
# Made module configurable from playbooks:
|
||||
# If etcd v2 running on host 192.168.1.21 on port 2379
|
||||
# we can use the following in a playbook to retrieve /tfm/network/config key
|
||||
#
|
||||
# - debug: msg={{lookup('etcd','/tfm/network/config', url='http://192.168.1.21:2379' , version='v2')}}
|
||||
#
|
||||
# Example Output:
|
||||
#
|
||||
# TASK [debug] *******************************************************************
|
||||
# ok: [localhost] => {
|
||||
# "msg": {
|
||||
# "Backend": {
|
||||
# "Type": "vxlan"
|
||||
# },
|
||||
# "Network": "172.30.0.0/16",
|
||||
# "SubnetLen": 24
|
||||
# }
|
||||
# }
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class Etcd:
|
||||
def __init__(self, url, version, validate_certs):
|
||||
self.url = url
|
||||
self.version = version
|
||||
self.baseurl = '%s/%s/keys' % (self.url, self.version)
|
||||
self.validate_certs = validate_certs
|
||||
|
||||
def _parse_node(self, node):
|
||||
# This function will receive all etcd tree,
|
||||
# if the level requested has any node, the recursion starts
|
||||
# create a list in the dir variable and it is passed to the
|
||||
# recursive function, and so on, if we get a variable,
|
||||
# the function will create a key-value at this level and
|
||||
# undoing the loop.
|
||||
path = {}
|
||||
if node.get('dir', False):
|
||||
for n in node.get('nodes', []):
|
||||
path[n['key'].split('/')[-1]] = self._parse_node(n)
|
||||
|
||||
else:
|
||||
path = node['value']
|
||||
|
||||
return path
|
||||
|
||||
def get(self, key):
|
||||
url = "%s/%s?recursive=true" % (self.baseurl, key)
|
||||
data = None
|
||||
value = {}
|
||||
try:
|
||||
r = open_url(url, validate_certs=self.validate_certs)
|
||||
data = r.read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
# I will not support Version 1 of etcd for folder parsing
|
||||
item = json.loads(data)
|
||||
if self.version == 'v1':
|
||||
# When ETCD are working with just v1
|
||||
if 'value' in item:
|
||||
value = item['value']
|
||||
else:
|
||||
if 'node' in item:
|
||||
# When a usual result from ETCD
|
||||
value = self._parse_node(item['node'])
|
||||
|
||||
if 'errorCode' in item:
|
||||
# Here return an error when an unknown entry responds
|
||||
value = "ENOENT"
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
validate_certs = self.get_option('validate_certs')
|
||||
url = self.get_option('url')
|
||||
version = self.get_option('version')
|
||||
|
||||
etcd = Etcd(url=url, version=version, validate_certs=validate_certs)
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
key = term.split()[0]
|
||||
value = etcd.get(key)
|
||||
ret.append(value)
|
||||
return ret
|
196
plugins/lookup/filetree.py
Normal file
196
plugins/lookup/filetree.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# (c) 2016 Dag Wieers <dag@wieers.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: filetree
|
||||
author: Dag Wieers (@dagwieers) <dag@wieers.com>
|
||||
short_description: recursively match all files in a directory tree
|
||||
description:
|
||||
- This lookup enables you to template a complete tree of files on a target system while retaining permissions and ownership.
|
||||
- Supports directories, files and symlinks, including SELinux and other file properties.
|
||||
- If you provide more than one path, it will implement a first_found logic, and will not process entries it already processed in previous paths.
|
||||
This enables merging different trees in order of importance, or add role_vars to specific paths to influence different instances of the same role.
|
||||
options:
|
||||
_terms:
|
||||
description: path(s) of files to read
|
||||
required: True
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create directories
|
||||
file:
|
||||
path: /web/{{ item.path }}
|
||||
state: directory
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'directory'
|
||||
|
||||
- name: Template files (explicitly skip directories in order to use the 'src' attribute)
|
||||
template:
|
||||
src: '{{ item.src }}'
|
||||
dest: /web/{{ item.path }}
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'file'
|
||||
|
||||
- name: Recreate symlinks
|
||||
file:
|
||||
src: '{{ item.src }}'
|
||||
dest: /web/{{ item.path }}
|
||||
state: link
|
||||
force: yes
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'link'
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: list of dictionaries with file information
|
||||
contains:
|
||||
src:
|
||||
description:
|
||||
- full path to file
|
||||
- not returned when C(item.state) is set to C(directory)
|
||||
root:
|
||||
description: allows filtering by original location
|
||||
path:
|
||||
description: contains the relative path to root
|
||||
mode:
|
||||
description: TODO
|
||||
state:
|
||||
description: TODO
|
||||
owner:
|
||||
description: TODO
|
||||
group:
|
||||
description: TODO
|
||||
seuser:
|
||||
description: TODO
|
||||
serole:
|
||||
description: TODO
|
||||
setype:
|
||||
description: TODO
|
||||
selevel:
|
||||
description: TODO
|
||||
uid:
|
||||
description: TODO
|
||||
gid:
|
||||
description: TODO
|
||||
size:
|
||||
description: TODO
|
||||
mtime:
|
||||
description: TODO
|
||||
ctime:
|
||||
description: TODO
|
||||
"""
|
||||
import os
|
||||
import pwd
|
||||
import grp
|
||||
import stat
|
||||
|
||||
HAVE_SELINUX = False
|
||||
try:
|
||||
import selinux
|
||||
HAVE_SELINUX = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
# If selinux fails to find a default, return an array of None
|
||||
def selinux_context(path):
|
||||
context = [None, None, None, None]
|
||||
if HAVE_SELINUX and selinux.is_selinux_enabled():
|
||||
try:
|
||||
# note: the selinux module uses byte strings on python2 and text
|
||||
# strings on python3
|
||||
ret = selinux.lgetfilecon_raw(to_native(path))
|
||||
except OSError:
|
||||
return context
|
||||
if ret[0] != -1:
|
||||
# Limit split to 4 because the selevel, the last in the list,
|
||||
# may contain ':' characters
|
||||
context = ret[1].split(':', 3)
|
||||
return context
|
||||
|
||||
|
||||
def file_props(root, path):
|
||||
''' Returns dictionary with file properties, or return None on failure '''
|
||||
abspath = os.path.join(root, path)
|
||||
|
||||
try:
|
||||
st = os.lstat(abspath)
|
||||
except OSError as e:
|
||||
display.warning('filetree: Error using stat() on path %s (%s)' % (abspath, e))
|
||||
return None
|
||||
|
||||
ret = dict(root=root, path=path)
|
||||
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
ret['state'] = 'link'
|
||||
ret['src'] = os.readlink(abspath)
|
||||
elif stat.S_ISDIR(st.st_mode):
|
||||
ret['state'] = 'directory'
|
||||
elif stat.S_ISREG(st.st_mode):
|
||||
ret['state'] = 'file'
|
||||
ret['src'] = abspath
|
||||
else:
|
||||
display.warning('filetree: Error file type of %s is not supported' % abspath)
|
||||
return None
|
||||
|
||||
ret['uid'] = st.st_uid
|
||||
ret['gid'] = st.st_gid
|
||||
try:
|
||||
ret['owner'] = pwd.getpwuid(st.st_uid).pw_name
|
||||
except KeyError:
|
||||
ret['owner'] = st.st_uid
|
||||
try:
|
||||
ret['group'] = to_text(grp.getgrgid(st.st_gid).gr_name)
|
||||
except KeyError:
|
||||
ret['group'] = st.st_gid
|
||||
ret['mode'] = '0%03o' % (stat.S_IMODE(st.st_mode))
|
||||
ret['size'] = st.st_size
|
||||
ret['mtime'] = st.st_mtime
|
||||
ret['ctime'] = st.st_ctime
|
||||
|
||||
if HAVE_SELINUX and selinux.is_selinux_enabled() == 1:
|
||||
context = selinux_context(abspath)
|
||||
ret['seuser'] = context[0]
|
||||
ret['serole'] = context[1]
|
||||
ret['setype'] = context[2]
|
||||
ret['selevel'] = context[3]
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
basedir = self.get_basedir(variables)
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
term_file = os.path.basename(term)
|
||||
dwimmed_path = self._loader.path_dwim_relative(basedir, 'files', os.path.dirname(term))
|
||||
path = os.path.join(dwimmed_path, term_file)
|
||||
display.debug("Walking '{0}'".format(path))
|
||||
for root, dirs, files in os.walk(path, topdown=True):
|
||||
for entry in dirs + files:
|
||||
relpath = os.path.relpath(os.path.join(root, entry), path)
|
||||
|
||||
# Skip if relpath was already processed (from another root)
|
||||
if relpath not in [entry['path'] for entry in ret]:
|
||||
props = file_props(path, relpath)
|
||||
if props is not None:
|
||||
display.debug(" found '{0}'".format(os.path.join(path, relpath)))
|
||||
ret.append(props)
|
||||
|
||||
return ret
|
83
plugins/lookup/flattened.py
Normal file
83
plugins/lookup/flattened.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
# (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: flattened
|
||||
author: Serge van Ginderachter <serge@vanginderachter.be>
|
||||
short_description: return single list completely flattened
|
||||
description:
|
||||
- given one or more lists, this lookup will flatten any list elements found recursively until only 1 list is left.
|
||||
options:
|
||||
_terms:
|
||||
description: lists to flatten
|
||||
required: True
|
||||
notes:
|
||||
- unlike 'items' which only flattens 1 level, this plugin will continue to flatten until it cannot find lists anymore.
|
||||
- aka highlander plugin, there can only be one (list).
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: "'unnest' all elements into single list"
|
||||
debug: msg="all in one list {{lookup('flattened', [1,2,3,[5,6]], [a,b,c], [[5,6,1,3], [34,a,b,c]])}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- flattened list
|
||||
type: list
|
||||
"""
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def _check_list_of_one_list(self, term):
|
||||
# make sure term is not a list of one (list of one..) item
|
||||
# return the final non list item if so
|
||||
|
||||
if isinstance(term, list) and len(term) == 1:
|
||||
term = term[0]
|
||||
if isinstance(term, list):
|
||||
term = self._check_list_of_one_list(term)
|
||||
|
||||
return term
|
||||
|
||||
def _do_flatten(self, terms, variables):
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
term = self._check_list_of_one_list(term)
|
||||
|
||||
if term == 'None' or term == 'null':
|
||||
# ignore undefined items
|
||||
break
|
||||
|
||||
if isinstance(term, string_types):
|
||||
# convert a variable to a list
|
||||
term2 = listify_lookup_plugin_terms(term, templar=self._templar, loader=self._loader)
|
||||
# but avoid converting a plain string to a list of one string
|
||||
if term2 != [term]:
|
||||
term = term2
|
||||
|
||||
if isinstance(term, list):
|
||||
# if it's a list, check recursively for items that are a list
|
||||
term = self._do_flatten(term, variables)
|
||||
ret.extend(term)
|
||||
else:
|
||||
ret.append(term)
|
||||
|
||||
return ret
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
raise AnsibleError("with_flattened expects a list")
|
||||
|
||||
return self._do_flatten(terms, variables)
|
138
plugins/lookup/gcp_storage_file.py
Normal file
138
plugins/lookup/gcp_storage_file.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
# (c) 2019, Eric Anderson <eric.sysmin@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: gcp_storage_file
|
||||
description:
|
||||
- This lookup returns the contents from a file residing on Google Cloud Storage
|
||||
short_description: Return GC Storage content
|
||||
author: Eric Anderson <eanderson@avinetworks.com>
|
||||
requirements:
|
||||
- python >= 2.6
|
||||
- requests >= 2.18.4
|
||||
- google-auth >= 1.3.0
|
||||
options:
|
||||
src:
|
||||
description:
|
||||
- Source location of file (may be local machine or cloud depending on action).
|
||||
required: false
|
||||
bucket:
|
||||
description:
|
||||
- The name of the bucket.
|
||||
required: false
|
||||
extends_documentation_fragment:
|
||||
- community.general.gcp
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- debug: msg="the value of foo.txt is {{ lookup('gcp_storage_file',
|
||||
bucket='gcp-bucket', src='mydir/foo.txt', project='project-name',
|
||||
auth_kind='serviceaccount', service_account_file='/tmp/myserviceaccountfile.json') }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description:
|
||||
- base64 encoded file content
|
||||
'''
|
||||
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import requests
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible_collections.google.cloud.plugins.module_utils.gcp_utils import navigate_hash, GcpSession
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class GcpMockModule(object):
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
|
||||
def fail_json(self, *args, **kwargs):
|
||||
raise AnsibleError(kwargs['msg'])
|
||||
|
||||
def raise_for_status(self, response):
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except getattr(requests.exceptions, 'RequestException'):
|
||||
self.fail_json(msg="GCP returned error: %s" % response.json())
|
||||
|
||||
|
||||
class GcpFileLookup():
|
||||
def get_file_contents(self, module):
|
||||
auth = GcpSession(module, 'storage')
|
||||
data = auth.get(self.media_link(module))
|
||||
return base64.b64encode(data.content.rstrip())
|
||||
|
||||
def fetch_resource(self, module, link, allow_not_found=True):
|
||||
auth = GcpSession(module, 'storage')
|
||||
return self.return_if_object(module, auth.get(link), allow_not_found)
|
||||
|
||||
def self_link(self, module):
|
||||
return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{src}".format(**module.params)
|
||||
|
||||
def media_link(self, module):
|
||||
return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{src}?alt=media".format(**module.params)
|
||||
|
||||
def return_if_object(self, module, response, allow_not_found=False):
|
||||
# If not found, return nothing.
|
||||
if allow_not_found and response.status_code == 404:
|
||||
return None
|
||||
# If no content, return nothing.
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
try:
|
||||
module.raise_for_status(response)
|
||||
result = response.json()
|
||||
except getattr(json.decoder, 'JSONDecodeError', ValueError) as inst:
|
||||
raise AnsibleError("Invalid JSON response with error: %s" % inst)
|
||||
if navigate_hash(result, ['error', 'errors']):
|
||||
raise AnsibleError(navigate_hash(result, ['error', 'errors']))
|
||||
return result
|
||||
|
||||
def object_headers(self, module):
|
||||
return {
|
||||
"name": module.params['src'],
|
||||
"Content-Type": mimetypes.guess_type(module.params['src'])[0],
|
||||
"Content-Length": str(os.path.getsize(module.params['src'])),
|
||||
}
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
params = {
|
||||
'bucket': kwargs.get('bucket', None),
|
||||
'src': kwargs.get('src', None),
|
||||
'projects': kwargs.get('projects', None),
|
||||
'scopes': kwargs.get('scopes', None),
|
||||
'zones': kwargs.get('zones', None),
|
||||
'auth_kind': kwargs.get('auth_kind', None),
|
||||
'service_account_file': kwargs.get('service_account_file', None),
|
||||
'service_account_email': kwargs.get('service_account_email', None),
|
||||
}
|
||||
|
||||
if not params['scopes']:
|
||||
params['scopes'] = ['https://www.googleapis.com/auth/devstorage.full_control']
|
||||
|
||||
fake_module = GcpMockModule(params)
|
||||
|
||||
# Check if files exist.
|
||||
remote_object = self.fetch_resource(fake_module, self.self_link(fake_module))
|
||||
if not remote_object:
|
||||
raise AnsibleError("File does not exist in bucket")
|
||||
|
||||
result = self.get_file_contents(fake_module)
|
||||
return [result]
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
return GcpFileLookup().run(terms, variables=variables, **kwargs)
|
300
plugins/lookup/hashi_vault.py
Normal file
300
plugins/lookup/hashi_vault.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
# (c) 2015, Jonathan Davila <jonathan(at)davila.io>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: hashi_vault
|
||||
author: Jonathan Davila <jdavila(at)ansible.com>
|
||||
short_description: retrieve secrets from HashiCorp's vault
|
||||
requirements:
|
||||
- hvac (python library)
|
||||
description:
|
||||
- retrieve secrets from HashiCorp's vault
|
||||
notes:
|
||||
- Due to a current limitation in the HVAC library there won't necessarily be an error if a bad endpoint is specified.
|
||||
- As of Ansible 2.10, only the latest secret is returned when specifying a KV v2 path.
|
||||
options:
|
||||
secret:
|
||||
description: query you are making.
|
||||
required: True
|
||||
token:
|
||||
description: vault token.
|
||||
env:
|
||||
- name: VAULT_TOKEN
|
||||
url:
|
||||
description: URL to vault service.
|
||||
env:
|
||||
- name: VAULT_ADDR
|
||||
default: 'http://127.0.0.1:8200'
|
||||
username:
|
||||
description: Authentication user name.
|
||||
password:
|
||||
description: Authentication password.
|
||||
role_id:
|
||||
description: Role id for a vault AppRole auth.
|
||||
env:
|
||||
- name: VAULT_ROLE_ID
|
||||
secret_id:
|
||||
description: Secret id for a vault AppRole auth.
|
||||
env:
|
||||
- name: VAULT_SECRET_ID
|
||||
auth_method:
|
||||
description:
|
||||
- Authentication method to be used.
|
||||
- C(userpass) is added in version 2.8.
|
||||
env:
|
||||
- name: VAULT_AUTH_METHOD
|
||||
choices:
|
||||
- userpass
|
||||
- ldap
|
||||
- approle
|
||||
mount_point:
|
||||
description: vault mount point, only required if you have a custom mount point.
|
||||
default: ldap
|
||||
ca_cert:
|
||||
description: path to certificate to use for authentication.
|
||||
aliases: [ cacert ]
|
||||
validate_certs:
|
||||
description: controls verification and validation of SSL certificates, mostly you only want to turn off with self signed ones.
|
||||
type: boolean
|
||||
default: True
|
||||
namespace:
|
||||
description: namespace where secrets reside. requires HVAC 0.7.0+ and Vault 0.11+.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200')}}"
|
||||
|
||||
- name: Return all secrets from a path
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200')}}"
|
||||
|
||||
- name: Vault that requires authentication via LDAP
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value auth_method=ldap mount_point=ldap username=myuser password=mypas url=http://myvault:8200')}}"
|
||||
|
||||
- name: Vault that requires authentication via username and password
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value auth_method=userpass username=myuser password=mypas url=http://myvault:8200')}}"
|
||||
|
||||
- name: Using an ssl vault
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hola:value token=c975b780-d1be-8016-866b-01d0f9b688a5 url=https://myvault:8200 validate_certs=False')}}"
|
||||
|
||||
- name: using certificate auth
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hi:value token=xxxx-xxx-xxx url=https://myvault:8200 validate_certs=True cacert=/cacert/path/ca.pem')}}"
|
||||
|
||||
- name: authenticate with a Vault app role
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value auth_method=approle role_id=myroleid secret_id=mysecretid url=http://myvault:8200')}}"
|
||||
|
||||
- name: Return all secrets from a path in a namespace
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/hello token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200 namespace=teama/admins')}}"
|
||||
|
||||
# When using KV v2 the PATH should include "data" between the secret engine mount and path (e.g. "secret/data/:path")
|
||||
# see: https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version
|
||||
- name: Return latest KV v2 secret from path
|
||||
debug:
|
||||
msg: "{{ lookup('hashi_vault', 'secret=secret/data/hello token=my_vault_token url=http://myvault_url:8200') }}"
|
||||
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- secrets(s) requested
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
HAS_HVAC = False
|
||||
try:
|
||||
import hvac
|
||||
HAS_HVAC = True
|
||||
except ImportError:
|
||||
HAS_HVAC = False
|
||||
|
||||
|
||||
ANSIBLE_HASHI_VAULT_ADDR = 'http://127.0.0.1:8200'
|
||||
|
||||
if os.getenv('VAULT_ADDR') is not None:
|
||||
ANSIBLE_HASHI_VAULT_ADDR = os.environ['VAULT_ADDR']
|
||||
|
||||
|
||||
class HashiVault:
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
self.url = kwargs.get('url', ANSIBLE_HASHI_VAULT_ADDR)
|
||||
self.namespace = kwargs.get('namespace', None)
|
||||
self.avail_auth_method = ['approle', 'userpass', 'ldap']
|
||||
|
||||
# split secret arg, which has format 'secret/hello:value' into secret='secret/hello' and secret_field='value'
|
||||
s = kwargs.get('secret')
|
||||
if s is None:
|
||||
raise AnsibleError("No secret specified for hashi_vault lookup")
|
||||
|
||||
s_f = s.rsplit(':', 1)
|
||||
self.secret = s_f[0]
|
||||
if len(s_f) >= 2:
|
||||
self.secret_field = s_f[1]
|
||||
else:
|
||||
self.secret_field = ''
|
||||
|
||||
self.verify = self.boolean_or_cacert(kwargs.get('validate_certs', True), kwargs.get('cacert', ''))
|
||||
|
||||
# If a particular backend is asked for (and its method exists) we call it, otherwise drop through to using
|
||||
# token auth. This means if a particular auth backend is requested and a token is also given, then we
|
||||
# ignore the token and attempt authentication against the specified backend.
|
||||
#
|
||||
# to enable a new auth backend, simply add a new 'def auth_<type>' method below.
|
||||
#
|
||||
self.auth_method = kwargs.get('auth_method', os.environ.get('VAULT_AUTH_METHOD'))
|
||||
self.verify = self.boolean_or_cacert(kwargs.get('validate_certs', True), kwargs.get('cacert', ''))
|
||||
if self.auth_method and self.auth_method != 'token':
|
||||
try:
|
||||
if self.namespace is not None:
|
||||
self.client = hvac.Client(url=self.url, verify=self.verify, namespace=self.namespace)
|
||||
else:
|
||||
self.client = hvac.Client(url=self.url, verify=self.verify)
|
||||
# prefixing with auth_ to limit which methods can be accessed
|
||||
getattr(self, 'auth_' + self.auth_method)(**kwargs)
|
||||
except AttributeError:
|
||||
raise AnsibleError("Authentication method '%s' not supported."
|
||||
" Available options are %r" % (self.auth_method, self.avail_auth_method))
|
||||
else:
|
||||
self.token = kwargs.get('token', os.environ.get('VAULT_TOKEN', None))
|
||||
if self.token is None and os.environ.get('HOME'):
|
||||
token_filename = os.path.join(
|
||||
os.environ.get('HOME'),
|
||||
'.vault-token'
|
||||
)
|
||||
if os.path.exists(token_filename):
|
||||
with open(token_filename) as token_file:
|
||||
self.token = token_file.read().strip()
|
||||
|
||||
if self.token is None:
|
||||
raise AnsibleError("No Vault Token specified")
|
||||
|
||||
if self.namespace is not None:
|
||||
self.client = hvac.Client(url=self.url, token=self.token, verify=self.verify, namespace=self.namespace)
|
||||
else:
|
||||
self.client = hvac.Client(url=self.url, token=self.token, verify=self.verify)
|
||||
|
||||
if not self.client.is_authenticated():
|
||||
raise AnsibleError("Invalid Hashicorp Vault Token Specified for hashi_vault lookup")
|
||||
|
||||
def get(self):
|
||||
data = self.client.read(self.secret)
|
||||
|
||||
# Check response for KV v2 fields and flatten nested secret data.
|
||||
#
|
||||
# https://vaultproject.io/api/secret/kv/kv-v2.html#sample-response-1
|
||||
try:
|
||||
# sentinel field checks
|
||||
check_dd = data['data']['data']
|
||||
check_md = data['data']['metadata']
|
||||
# unwrap nested data
|
||||
data = data['data']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if data is None:
|
||||
raise AnsibleError("The secret %s doesn't seem to exist for hashi_vault lookup" % self.secret)
|
||||
|
||||
if self.secret_field == '':
|
||||
return data['data']
|
||||
|
||||
if self.secret_field not in data['data']:
|
||||
raise AnsibleError("The secret %s does not contain the field '%s'. for hashi_vault lookup" % (self.secret, self.secret_field))
|
||||
|
||||
return data['data'][self.secret_field]
|
||||
|
||||
def check_params(self, **kwargs):
|
||||
username = kwargs.get('username')
|
||||
if username is None:
|
||||
raise AnsibleError("Authentication method %s requires a username" % self.auth_method)
|
||||
|
||||
password = kwargs.get('password')
|
||||
if password is None:
|
||||
raise AnsibleError("Authentication method %s requires a password" % self.auth_method)
|
||||
|
||||
mount_point = kwargs.get('mount_point')
|
||||
|
||||
return username, password, mount_point
|
||||
|
||||
def auth_userpass(self, **kwargs):
|
||||
username, password, mount_point = self.check_params(**kwargs)
|
||||
if mount_point is None:
|
||||
mount_point = 'userpass'
|
||||
|
||||
self.client.auth_userpass(username, password, mount_point=mount_point)
|
||||
|
||||
def auth_ldap(self, **kwargs):
|
||||
username, password, mount_point = self.check_params(**kwargs)
|
||||
if mount_point is None:
|
||||
mount_point = 'ldap'
|
||||
|
||||
self.client.auth.ldap.login(username, password, mount_point=mount_point)
|
||||
|
||||
def boolean_or_cacert(self, validate_certs, cacert):
|
||||
validate_certs = boolean(validate_certs, strict=False)
|
||||
'''' return a bool or cacert '''
|
||||
if validate_certs is True:
|
||||
if cacert != '':
|
||||
return cacert
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def auth_approle(self, **kwargs):
|
||||
role_id = kwargs.get('role_id', os.environ.get('VAULT_ROLE_ID', None))
|
||||
if role_id is None:
|
||||
raise AnsibleError("Authentication method app role requires a role_id")
|
||||
|
||||
secret_id = kwargs.get('secret_id', os.environ.get('VAULT_SECRET_ID', None))
|
||||
if secret_id is None:
|
||||
raise AnsibleError("Authentication method app role requires a secret_id")
|
||||
|
||||
self.client.auth_approle(role_id, secret_id)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
if not HAS_HVAC:
|
||||
raise AnsibleError("Please pip install hvac to use the hashi_vault lookup module.")
|
||||
|
||||
vault_args = terms[0].split()
|
||||
vault_dict = {}
|
||||
ret = []
|
||||
|
||||
for param in vault_args:
|
||||
try:
|
||||
key, value = param.split('=')
|
||||
except ValueError:
|
||||
raise AnsibleError("hashi_vault lookup plugin needs key=value pairs, but received %s" % terms)
|
||||
vault_dict[key] = value
|
||||
|
||||
if 'ca_cert' in vault_dict.keys():
|
||||
vault_dict['cacert'] = vault_dict['ca_cert']
|
||||
vault_dict.pop('ca_cert', None)
|
||||
|
||||
vault_conn = HashiVault(**vault_dict)
|
||||
|
||||
for term in terms:
|
||||
key = term.split()[0]
|
||||
value = vault_conn.get()
|
||||
ret.append(value)
|
||||
|
||||
return ret
|
86
plugins/lookup/hiera.py
Normal file
86
plugins/lookup/hiera.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# (c) 2017, Juan Manuel Parrilla <jparrill@redhat.com>
|
||||
# (c) 2012-17 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Juan Manuel Parrilla (@jparrill)
|
||||
lookup: hiera
|
||||
short_description: get info from hiera data
|
||||
requirements:
|
||||
- hiera (command line utility)
|
||||
description:
|
||||
- Retrieves data from an Puppetmaster node using Hiera as ENC
|
||||
options:
|
||||
_hiera_key:
|
||||
description:
|
||||
- The list of keys to lookup on the Puppetmaster
|
||||
type: list
|
||||
element_type: string
|
||||
required: True
|
||||
_bin_file:
|
||||
description:
|
||||
- Binary file to execute Hiera
|
||||
default: '/usr/bin/hiera'
|
||||
env:
|
||||
- name: ANSIBLE_HIERA_BIN
|
||||
_hierarchy_file:
|
||||
description:
|
||||
- File that describes the hierarchy of Hiera
|
||||
default: '/etc/hiera.yaml'
|
||||
env:
|
||||
- name: ANSIBLE_HIERA_CFG
|
||||
# FIXME: incomplete options .. _terms? environment/fqdn?
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
# All this examples depends on hiera.yml that describes the hierarchy
|
||||
|
||||
- name: "a value from Hiera 'DB'"
|
||||
debug: msg={{ lookup('hiera', 'foo') }}
|
||||
|
||||
- name: "a value from a Hiera 'DB' on other environment"
|
||||
debug: msg={{ lookup('hiera', 'foo environment=production') }}
|
||||
|
||||
- name: "a value from a Hiera 'DB' for a concrete node"
|
||||
debug: msg={{ lookup('hiera', 'foo fqdn=puppet01.localdomain') }}
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- a value associated with input key
|
||||
type: strings
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.cmd_functions import run_cmd
|
||||
|
||||
ANSIBLE_HIERA_CFG = os.getenv('ANSIBLE_HIERA_CFG', '/etc/hiera.yaml')
|
||||
ANSIBLE_HIERA_BIN = os.getenv('ANSIBLE_HIERA_BIN', '/usr/bin/hiera')
|
||||
|
||||
|
||||
class Hiera(object):
|
||||
def get(self, hiera_key):
|
||||
pargs = [ANSIBLE_HIERA_BIN]
|
||||
pargs.extend(['-c', ANSIBLE_HIERA_CFG])
|
||||
|
||||
pargs.extend(hiera_key)
|
||||
|
||||
rc, output, err = run_cmd("{0} -c {1} {2}".format(
|
||||
ANSIBLE_HIERA_BIN, ANSIBLE_HIERA_CFG, hiera_key[0]))
|
||||
|
||||
return output.strip()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=''):
|
||||
hiera = Hiera()
|
||||
ret = []
|
||||
|
||||
ret.append(hiera.get(terms))
|
||||
return ret
|
65
plugins/lookup/keyring.py
Normal file
65
plugins/lookup/keyring.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
# (c) 2016, Samuel Boucher <boucher.samuel.c@gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: keyring
|
||||
author:
|
||||
- Samuel Boucher <boucher.samuel.c@gmail.com>
|
||||
requirements:
|
||||
- keyring (python library)
|
||||
short_description: grab secrets from the OS keyring
|
||||
description:
|
||||
- Allows you to access data stored in the OS provided keyring/keychain.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name : output secrets to screen (BAD IDEA)
|
||||
debug:
|
||||
msg: "Password: {{item}}"
|
||||
with_keyring:
|
||||
- 'servicename username'
|
||||
|
||||
- name: access mysql with password from keyring
|
||||
mysql_db: login_password={{lookup('keyring','mysql joe')}} login_user=joe
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: secrets stored
|
||||
"""
|
||||
|
||||
HAS_KEYRING = True
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.utils.display import Display
|
||||
|
||||
try:
|
||||
import keyring
|
||||
except ImportError:
|
||||
HAS_KEYRING = False
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, **kwargs):
|
||||
if not HAS_KEYRING:
|
||||
raise AnsibleError(u"Can't LOOKUP(keyring): missing required python library 'keyring'")
|
||||
|
||||
display.vvvv(u"keyring: %s" % keyring.get_keyring())
|
||||
ret = []
|
||||
for term in terms:
|
||||
(servicename, username) = (term.split()[0], term.split()[1])
|
||||
display.vvvv(u"username: %s, servicename: %s " % (username, servicename))
|
||||
password = keyring.get_password(servicename, username)
|
||||
if password is None:
|
||||
raise AnsibleError(u"servicename: %s for user %s not found" % (servicename, username))
|
||||
ret.append(password.rstrip())
|
||||
return ret
|
97
plugins/lookup/lastpass.py
Normal file
97
plugins/lookup/lastpass.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
# (c) 2016, Andrew Zenk <azenk@umn.edu>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: lastpass
|
||||
author:
|
||||
- Andrew Zenk <azenk@umn.edu>
|
||||
requirements:
|
||||
- lpass (command line utility)
|
||||
- must have already logged into lastpass
|
||||
short_description: fetch data from lastpass
|
||||
description:
|
||||
- use the lpass command line utility to fetch specific fields from lastpass
|
||||
options:
|
||||
_terms:
|
||||
description: key from which you want to retrieve the field
|
||||
required: True
|
||||
field:
|
||||
description: field to return from lastpass
|
||||
default: 'password'
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: get 'custom_field' from lastpass entry 'entry-name'
|
||||
debug:
|
||||
msg: "{{ lookup('lastpass', 'entry-name', field='custom_field') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: secrets stored
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class LPassException(AnsibleError):
|
||||
pass
|
||||
|
||||
|
||||
class LPass(object):
|
||||
|
||||
def __init__(self, path='lpass'):
|
||||
self._cli_path = path
|
||||
|
||||
@property
|
||||
def cli_path(self):
|
||||
return self._cli_path
|
||||
|
||||
@property
|
||||
def logged_in(self):
|
||||
out, err = self._run(self._build_args("logout"), stdin="n\n", expected_rc=1)
|
||||
return err.startswith("Are you sure you would like to log out?")
|
||||
|
||||
def _run(self, args, stdin=None, expected_rc=0):
|
||||
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
out, err = p.communicate(to_bytes(stdin))
|
||||
rc = p.wait()
|
||||
if rc != expected_rc:
|
||||
raise LPassException(err)
|
||||
return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')
|
||||
|
||||
def _build_args(self, command, args=None):
|
||||
if args is None:
|
||||
args = []
|
||||
args = [command] + args
|
||||
args += ["--color=never"]
|
||||
return args
|
||||
|
||||
def get_field(self, key, field):
|
||||
if field in ['username', 'password', 'url', 'notes', 'id', 'name']:
|
||||
out, err = self._run(self._build_args("show", ["--{0}".format(field), key]))
|
||||
else:
|
||||
out, err = self._run(self._build_args("show", ["--field={0}".format(field), key]))
|
||||
return out.strip()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
lp = LPass()
|
||||
|
||||
if not lp.logged_in:
|
||||
raise AnsibleError("Not logged into lastpass: please run 'lpass login' first")
|
||||
|
||||
field = kwargs.get('field', 'password')
|
||||
values = []
|
||||
for term in terms:
|
||||
values.append(lp.get_field(term, field))
|
||||
return values
|
117
plugins/lookup/lmdb_kv.py
Normal file
117
plugins/lookup/lmdb_kv.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# (c) 2017-2018, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: lmdb_kv
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens)
|
||||
short_description: fetch data from LMDB
|
||||
description:
|
||||
- This lookup returns a list of results from an LMDB DB corresponding to a list of items given to it
|
||||
requirements:
|
||||
- lmdb (python library https://lmdb.readthedocs.io/en/release/)
|
||||
options:
|
||||
_terms:
|
||||
description: list of keys to query
|
||||
db:
|
||||
description: path to LMDB database
|
||||
default: 'ansible.mdb'
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: query LMDB for a list of country codes
|
||||
debug:
|
||||
msg: "{{ query('lmdb_kv', 'nl', 'be', 'lu', db='jp.mdb') }}"
|
||||
|
||||
- name: use list of values in a loop by key wildcard
|
||||
debug:
|
||||
msg: "Hello from {{ item.0 }} a.k.a. {{ item.1 }}"
|
||||
vars:
|
||||
- lmdb_kv_db: jp.mdb
|
||||
with_lmdb_kv:
|
||||
- "n*"
|
||||
|
||||
- name: get an item by key
|
||||
assert:
|
||||
that:
|
||||
- item == 'Belgium'
|
||||
vars:
|
||||
- lmdb_kv_db: jp.mdb
|
||||
with_lmdb_kv:
|
||||
- be
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: value(s) stored in LMDB
|
||||
"""
|
||||
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
HAVE_LMDB = True
|
||||
try:
|
||||
import lmdb
|
||||
except ImportError:
|
||||
HAVE_LMDB = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
'''
|
||||
terms contain any number of keys to be retrieved.
|
||||
If terms is None, all keys from the database are returned
|
||||
with their values, and if term ends in an asterisk, we
|
||||
start searching there
|
||||
|
||||
The LMDB database defaults to 'ansible.mdb' if Ansible's
|
||||
variable 'lmdb_kv_db' is not set:
|
||||
|
||||
vars:
|
||||
- lmdb_kv_db: "jp.mdb"
|
||||
'''
|
||||
|
||||
if HAVE_LMDB is False:
|
||||
raise AnsibleError("Can't LOOKUP(lmdb_kv): this module requires lmdb to be installed")
|
||||
|
||||
db = variables.get('lmdb_kv_db', None)
|
||||
if db is None:
|
||||
db = kwargs.get('db', 'ansible.mdb')
|
||||
db = str(db)
|
||||
|
||||
try:
|
||||
env = lmdb.open(db, readonly=True)
|
||||
except Exception as e:
|
||||
raise AnsibleError("LMDB can't open database %s: %s" % (db, to_native(e)))
|
||||
|
||||
ret = []
|
||||
if len(terms) == 0:
|
||||
with env.begin() as txn:
|
||||
cursor = txn.cursor()
|
||||
cursor.first()
|
||||
for key, value in cursor:
|
||||
ret.append((to_text(key), to_native(value)))
|
||||
|
||||
else:
|
||||
for term in terms:
|
||||
with env.begin() as txn:
|
||||
if term.endswith('*'):
|
||||
cursor = txn.cursor()
|
||||
prefix = term[:-1] # strip asterisk
|
||||
cursor.set_range(to_text(term).encode())
|
||||
while cursor.key().startswith(to_text(prefix).encode()):
|
||||
for key, value in cursor:
|
||||
ret.append((to_text(key), to_native(value)))
|
||||
cursor.next()
|
||||
else:
|
||||
value = txn.get(to_text(term).encode())
|
||||
if value is not None:
|
||||
ret.append(to_native(value))
|
||||
|
||||
return ret
|
275
plugins/lookup/manifold.py
Normal file
275
plugins/lookup/manifold.py
Normal file
|
@ -0,0 +1,275 @@
|
|||
# (c) 2018, Arigato Machine Inc.
|
||||
# (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Kyrylo Galanov (galanoff@gmail.com)
|
||||
lookup: manifold
|
||||
short_description: get credentials from Manifold.co
|
||||
description:
|
||||
- Retrieves resources' credentials from Manifold.co
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- Optional list of resource labels to lookup on Manifold.co. If no resources are specified, all
|
||||
matched resources will be returned.
|
||||
type: list
|
||||
elements: string
|
||||
required: False
|
||||
api_token:
|
||||
description:
|
||||
- manifold API token
|
||||
type: string
|
||||
required: True
|
||||
env:
|
||||
- name: MANIFOLD_API_TOKEN
|
||||
project:
|
||||
description:
|
||||
- The project label you want to get the resource for.
|
||||
type: string
|
||||
required: False
|
||||
team:
|
||||
description:
|
||||
- The team label you want to get the resource for.
|
||||
type: string
|
||||
required: False
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: all available resources
|
||||
debug: msg="{{ lookup('manifold', api_token='SecretToken') }}"
|
||||
- name: all available resources for a specific project in specific team
|
||||
debug: msg="{{ lookup('manifold', api_token='SecretToken', project='poject-1', team='team-2') }}"
|
||||
- name: two specific resources
|
||||
debug: msg="{{ lookup('manifold', 'resource-1', 'resource-2') }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description:
|
||||
- dictionary of credentials ready to be consumed as environment variables. If multiple resources define
|
||||
the same environment variable(s), the last one returned by the Manifold API will take precedence.
|
||||
type: dict
|
||||
'''
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils import six
|
||||
from ansible.utils.display import Display
|
||||
from traceback import format_exception
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ManifoldApiClient(object):
|
||||
base_url = 'https://api.{api}.manifold.co/v1/{endpoint}'
|
||||
http_agent = 'python-manifold-ansible-1.0.0'
|
||||
|
||||
def __init__(self, token):
|
||||
self._token = token
|
||||
|
||||
def request(self, api, endpoint, *args, **kwargs):
|
||||
"""
|
||||
Send a request to API backend and pre-process a response.
|
||||
:param api: API to send a request to
|
||||
:type api: str
|
||||
:param endpoint: API endpoint to fetch data from
|
||||
:type endpoint: str
|
||||
:param args: other args for open_url
|
||||
:param kwargs: other kwargs for open_url
|
||||
:return: server response. JSON response is automatically deserialized.
|
||||
:rtype: dict | list | str
|
||||
"""
|
||||
|
||||
default_headers = {
|
||||
'Authorization': "Bearer {0}".format(self._token),
|
||||
'Accept': "*/*" # Otherwise server doesn't set content-type header
|
||||
}
|
||||
|
||||
url = self.base_url.format(api=api, endpoint=endpoint)
|
||||
|
||||
headers = default_headers
|
||||
arg_headers = kwargs.pop('headers', None)
|
||||
if arg_headers:
|
||||
headers.update(arg_headers)
|
||||
|
||||
try:
|
||||
display.vvvv('manifold lookup connecting to {0}'.format(url))
|
||||
response = open_url(url, headers=headers, http_agent=self.http_agent, *args, **kwargs)
|
||||
data = response.read()
|
||||
if response.headers.get('content-type') == 'application/json':
|
||||
data = json.loads(data)
|
||||
return data
|
||||
except ValueError:
|
||||
raise ApiError('JSON response can\'t be parsed while requesting {url}:\n{json}'.format(json=data, url=url))
|
||||
except HTTPError as e:
|
||||
raise ApiError('Server returned: {err} while requesting {url}:\n{response}'.format(
|
||||
err=str(e), url=url, response=e.read()))
|
||||
except URLError as e:
|
||||
raise ApiError('Failed lookup url for {url} : {err}'.format(url=url, err=str(e)))
|
||||
except SSLValidationError as e:
|
||||
raise ApiError('Error validating the server\'s certificate for {url}: {err}'.format(url=url, err=str(e)))
|
||||
except ConnectionError as e:
|
||||
raise ApiError('Error connecting to {url}: {err}'.format(url=url, err=str(e)))
|
||||
|
||||
def get_resources(self, team_id=None, project_id=None, label=None):
|
||||
"""
|
||||
Get resources list
|
||||
:param team_id: ID of the Team to filter resources by
|
||||
:type team_id: str
|
||||
:param project_id: ID of the project to filter resources by
|
||||
:type project_id: str
|
||||
:param label: filter resources by a label, returns a list with one or zero elements
|
||||
:type label: str
|
||||
:return: list of resources
|
||||
:rtype: list
|
||||
"""
|
||||
api = 'marketplace'
|
||||
endpoint = 'resources'
|
||||
query_params = {}
|
||||
|
||||
if team_id:
|
||||
query_params['team_id'] = team_id
|
||||
if project_id:
|
||||
query_params['project_id'] = project_id
|
||||
if label:
|
||||
query_params['label'] = label
|
||||
|
||||
if query_params:
|
||||
endpoint += '?' + urlencode(query_params)
|
||||
|
||||
return self.request(api, endpoint)
|
||||
|
||||
def get_teams(self, label=None):
|
||||
"""
|
||||
Get teams list
|
||||
:param label: filter teams by a label, returns a list with one or zero elements
|
||||
:type label: str
|
||||
:return: list of teams
|
||||
:rtype: list
|
||||
"""
|
||||
api = 'identity'
|
||||
endpoint = 'teams'
|
||||
data = self.request(api, endpoint)
|
||||
# Label filtering is not supported by API, however this function provides uniform interface
|
||||
if label:
|
||||
data = list(filter(lambda x: x['body']['label'] == label, data))
|
||||
return data
|
||||
|
||||
def get_projects(self, label=None):
|
||||
"""
|
||||
Get projects list
|
||||
:param label: filter projects by a label, returns a list with one or zero elements
|
||||
:type label: str
|
||||
:return: list of projects
|
||||
:rtype: list
|
||||
"""
|
||||
api = 'marketplace'
|
||||
endpoint = 'projects'
|
||||
query_params = {}
|
||||
|
||||
if label:
|
||||
query_params['label'] = label
|
||||
|
||||
if query_params:
|
||||
endpoint += '?' + urlencode(query_params)
|
||||
|
||||
return self.request(api, endpoint)
|
||||
|
||||
def get_credentials(self, resource_id):
|
||||
"""
|
||||
Get resource credentials
|
||||
:param resource_id: ID of the resource to filter credentials by
|
||||
:type resource_id: str
|
||||
:return:
|
||||
"""
|
||||
api = 'marketplace'
|
||||
endpoint = 'credentials?' + urlencode({'resource_id': resource_id})
|
||||
return self.request(api, endpoint)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, api_token=None, project=None, team=None):
|
||||
"""
|
||||
:param terms: a list of resources lookups to run.
|
||||
:param variables: ansible variables active at the time of the lookup
|
||||
:param api_token: API token
|
||||
:param project: optional project label
|
||||
:param team: optional team label
|
||||
:return: a dictionary of resources credentials
|
||||
"""
|
||||
|
||||
if not api_token:
|
||||
api_token = os.getenv('MANIFOLD_API_TOKEN')
|
||||
if not api_token:
|
||||
raise AnsibleError('API token is required. Please set api_token parameter or MANIFOLD_API_TOKEN env var')
|
||||
|
||||
try:
|
||||
labels = terms
|
||||
client = ManifoldApiClient(api_token)
|
||||
|
||||
if team:
|
||||
team_data = client.get_teams(team)
|
||||
if len(team_data) == 0:
|
||||
raise AnsibleError("Team '{0}' does not exist".format(team))
|
||||
team_id = team_data[0]['id']
|
||||
else:
|
||||
team_id = None
|
||||
|
||||
if project:
|
||||
project_data = client.get_projects(project)
|
||||
if len(project_data) == 0:
|
||||
raise AnsibleError("Project '{0}' does not exist".format(project))
|
||||
project_id = project_data[0]['id']
|
||||
else:
|
||||
project_id = None
|
||||
|
||||
if len(labels) == 1: # Use server-side filtering if one resource is requested
|
||||
resources_data = client.get_resources(team_id=team_id, project_id=project_id, label=labels[0])
|
||||
else: # Get all resources and optionally filter labels
|
||||
resources_data = client.get_resources(team_id=team_id, project_id=project_id)
|
||||
if labels:
|
||||
resources_data = list(filter(lambda x: x['body']['label'] in labels, resources_data))
|
||||
|
||||
if labels and len(resources_data) < len(labels):
|
||||
fetched_labels = [r['body']['label'] for r in resources_data]
|
||||
not_found_labels = [label for label in labels if label not in fetched_labels]
|
||||
raise AnsibleError("Resource(s) {0} do not exist".format(', '.join(not_found_labels)))
|
||||
|
||||
credentials = {}
|
||||
cred_map = {}
|
||||
for resource in resources_data:
|
||||
resource_credentials = client.get_credentials(resource['id'])
|
||||
if len(resource_credentials) and resource_credentials[0]['body']['values']:
|
||||
for cred_key, cred_val in six.iteritems(resource_credentials[0]['body']['values']):
|
||||
label = resource['body']['label']
|
||||
if cred_key in credentials:
|
||||
display.warning("'{cred_key}' with label '{old_label}' was replaced by resource data "
|
||||
"with label '{new_label}'".format(cred_key=cred_key,
|
||||
old_label=cred_map[cred_key],
|
||||
new_label=label))
|
||||
credentials[cred_key] = cred_val
|
||||
cred_map[cred_key] = label
|
||||
|
||||
ret = [credentials]
|
||||
return ret
|
||||
except ApiError as e:
|
||||
raise AnsibleError('API Error: {0}'.format(str(e)))
|
||||
except AnsibleError as e:
|
||||
raise e
|
||||
except Exception:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
raise AnsibleError(format_exception(exc_type, exc_value, exc_traceback))
|
118
plugins/lookup/nios.py
Normal file
118
plugins/lookup/nios.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
#
|
||||
# Copyright 2018 Red Hat | Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
lookup: nios
|
||||
short_description: Query Infoblox NIOS objects
|
||||
description:
|
||||
- Uses the Infoblox WAPI API to fetch NIOS specified objects. This lookup
|
||||
supports adding additional keywords to filter the return data and specify
|
||||
the desired set of returned fields.
|
||||
requirements:
|
||||
- infoblox-client
|
||||
extends_documentation_fragment:
|
||||
- community.general.nios
|
||||
|
||||
options:
|
||||
_terms:
|
||||
description: The name of the object to return from NIOS
|
||||
required: True
|
||||
return_fields:
|
||||
description: The list of field names to return for the specified object.
|
||||
filter:
|
||||
description: a dict object that is used to filter the return objects
|
||||
extattrs:
|
||||
description: a dict object that is used to filter on extattrs
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: fetch all networkview objects
|
||||
set_fact:
|
||||
networkviews: "{{ lookup('nios', 'networkview', provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
- name: fetch the default dns view
|
||||
set_fact:
|
||||
dns_views: "{{ lookup('nios', 'view', filter={'name': 'default'}, provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
# all of the examples below use credentials that are set using env variables
|
||||
# export INFOBLOX_HOST=nios01
|
||||
# export INFOBLOX_USERNAME=admin
|
||||
# export INFOBLOX_PASSWORD=admin
|
||||
|
||||
- name: fetch all host records and include extended attributes
|
||||
set_fact:
|
||||
host_records: "{{ lookup('nios', 'record:host', return_fields=['extattrs', 'name', 'view', 'comment']}) }}"
|
||||
|
||||
|
||||
- name: use env variables to pass credentials
|
||||
set_fact:
|
||||
networkviews: "{{ lookup('nios', 'networkview') }}"
|
||||
|
||||
- name: get a host record
|
||||
set_fact:
|
||||
host: "{{ lookup('nios', 'record:host', filter={'name': 'hostname.ansible.com'}) }}"
|
||||
|
||||
- name: get the authoritative zone from a non default dns view
|
||||
set_fact:
|
||||
host: "{{ lookup('nios', 'zone_auth', filter={'fqdn': 'ansible.com', 'view': 'ansible-dns'}) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
obj_type:
|
||||
description:
|
||||
- The object type specified in the terms argument
|
||||
returned: always
|
||||
type: complex
|
||||
contains:
|
||||
obj_field:
|
||||
- One or more obj_type fields as specified by return_fields argument or
|
||||
the default set of fields as per the object type
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiLookup
|
||||
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import normalize_extattrs, flatten_extattrs
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
try:
|
||||
obj_type = terms[0]
|
||||
except IndexError:
|
||||
raise AnsibleError('the object_type must be specified')
|
||||
|
||||
return_fields = kwargs.pop('return_fields', None)
|
||||
filter_data = kwargs.pop('filter', {})
|
||||
extattrs = normalize_extattrs(kwargs.pop('extattrs', {}))
|
||||
provider = kwargs.pop('provider', {})
|
||||
wapi = WapiLookup(provider)
|
||||
res = wapi.get_object(obj_type, filter_data, return_fields=return_fields, extattrs=extattrs)
|
||||
if res is not None:
|
||||
for obj in res:
|
||||
if 'extattrs' in obj:
|
||||
obj['extattrs'] = flatten_extattrs(obj['extattrs'])
|
||||
else:
|
||||
res = []
|
||||
return res
|
100
plugins/lookup/nios_next_ip.py
Normal file
100
plugins/lookup/nios_next_ip.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
#
|
||||
# Copyright 2018 Red Hat | Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
lookup: nios_next_ip
|
||||
short_description: Return the next available IP address for a network
|
||||
description:
|
||||
- Uses the Infoblox WAPI API to return the next available IP addresses
|
||||
for a given network CIDR
|
||||
requirements:
|
||||
- infoblox-client
|
||||
extends_documentation_fragment:
|
||||
- community.general.nios
|
||||
|
||||
options:
|
||||
_terms:
|
||||
description: The CIDR network to retrieve the next addresses from
|
||||
required: True
|
||||
num:
|
||||
description: The number of IP addresses to return
|
||||
required: false
|
||||
default: 1
|
||||
exclude:
|
||||
description: List of IP's that need to be excluded from returned IP addresses
|
||||
required: false
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: return next available IP address for network 192.168.10.0/24
|
||||
set_fact:
|
||||
ipaddr: "{{ lookup('nios_next_ip', '192.168.10.0/24', provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
- name: return the next 3 available IP addresses for network 192.168.10.0/24
|
||||
set_fact:
|
||||
ipaddr: "{{ lookup('nios_next_ip', '192.168.10.0/24', num=3, provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
- name: return the next 3 available IP addresses for network 192.168.10.0/24 excluding ip addresses - ['192.168.10.1', '192.168.10.2']
|
||||
set_fact:
|
||||
ipaddr: "{{ lookup('nios_next_ip', '192.168.10.0/24', num=3, exclude=['192.168.10.1', '192.168.10.2'],
|
||||
provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- The list of next IP addresses available
|
||||
returned: always
|
||||
type: list
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiLookup
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
try:
|
||||
network = terms[0]
|
||||
except IndexError:
|
||||
raise AnsibleError('missing argument in the form of A.B.C.D/E')
|
||||
|
||||
provider = kwargs.pop('provider', {})
|
||||
wapi = WapiLookup(provider)
|
||||
|
||||
network_obj = wapi.get_object('network', {'network': network})
|
||||
if network_obj is None:
|
||||
raise AnsibleError('unable to find network object %s' % network)
|
||||
|
||||
num = kwargs.get('num', 1)
|
||||
exclude_ip = kwargs.get('exclude', [])
|
||||
|
||||
try:
|
||||
ref = network_obj[0]['_ref']
|
||||
avail_ips = wapi.call_func('next_available_ip', ref, {'num': num, 'exclude': exclude_ip})
|
||||
return [avail_ips['ips']]
|
||||
except Exception as exc:
|
||||
raise AnsibleError(to_text(exc))
|
112
plugins/lookup/nios_next_network.py
Normal file
112
plugins/lookup/nios_next_network.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
#
|
||||
# Copyright 2018 Red Hat | Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
lookup: nios_next_network
|
||||
short_description: Return the next available network range for a network-container
|
||||
description:
|
||||
- Uses the Infoblox WAPI API to return the next available network addresses for
|
||||
a given network CIDR
|
||||
requirements:
|
||||
- infoblox_client
|
||||
extends_documentation_fragment:
|
||||
- community.general.nios
|
||||
|
||||
options:
|
||||
_terms:
|
||||
description: The CIDR network to retrieve the next network from next available network within the specified
|
||||
container.
|
||||
required: True
|
||||
cidr:
|
||||
description:
|
||||
- The CIDR of the network to retrieve the next network from next available network within the
|
||||
specified container. Also, Requested CIDR must be specified and greater than the parent CIDR.
|
||||
required: True
|
||||
default: 24
|
||||
num:
|
||||
description: The number of network addresses to return from network-container
|
||||
required: false
|
||||
default: 1
|
||||
exclude:
|
||||
description: Network addresses returned from network-container excluding list of user's input network range
|
||||
required: false
|
||||
default: ''
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: return next available network for network-container 192.168.10.0/24
|
||||
set_fact:
|
||||
networkaddr: "{{ lookup('nios_next_network', '192.168.10.0/24', cidr=25, provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
- name: return the next 2 available network addresses for network-container 192.168.10.0/24
|
||||
set_fact:
|
||||
networkaddr: "{{ lookup('nios_next_network', '192.168.10.0/24', cidr=25, num=2,
|
||||
provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
|
||||
- name: return the available network addresses for network-container 192.168.10.0/24 excluding network range '192.168.10.0/25'
|
||||
set_fact:
|
||||
networkaddr: "{{ lookup('nios_next_network', '192.168.10.0/24', cidr=25, exclude=['192.168.10.0/25'],
|
||||
provider={'host': 'nios01', 'username': 'admin', 'password': 'password'}) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- The list of next network addresses available
|
||||
returned: always
|
||||
type: list
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible_collections.community.general.plugins.module_utils.net_tools.nios.api import WapiLookup
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
try:
|
||||
network = terms[0]
|
||||
except IndexError:
|
||||
raise AnsibleError('missing network argument in the form of A.B.C.D/E')
|
||||
try:
|
||||
cidr = kwargs.get('cidr', 24)
|
||||
except IndexError:
|
||||
raise AnsibleError('missing CIDR argument in the form of xx')
|
||||
|
||||
provider = kwargs.pop('provider', {})
|
||||
wapi = WapiLookup(provider)
|
||||
network_obj = wapi.get_object('networkcontainer', {'network': network})
|
||||
|
||||
if network_obj is None:
|
||||
raise AnsibleError('unable to find network-container object %s' % network)
|
||||
num = kwargs.get('num', 1)
|
||||
exclude_ip = kwargs.get('exclude', [])
|
||||
|
||||
try:
|
||||
ref = network_obj[0]['_ref']
|
||||
avail_nets = wapi.call_func('next_available_network', ref, {'cidr': cidr, 'num': num, 'exclude': exclude_ip})
|
||||
return [avail_nets['networks']]
|
||||
except Exception as exc:
|
||||
raise AnsibleError(to_text(exc))
|
226
plugins/lookup/onepassword.py
Normal file
226
plugins/lookup/onepassword.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Scott Buchanan <sbuchanan@ri.pn>
|
||||
# Copyright: (c) 2016, Andrew Zenk <azenk@umn.edu> (lastpass.py used as starting point)
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: onepassword
|
||||
author:
|
||||
- Scott Buchanan (@scottsb)
|
||||
- Andrew Zenk (@azenk)
|
||||
- Sam Doran (@samdoran)
|
||||
requirements:
|
||||
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
|
||||
short_description: fetch field values from 1Password
|
||||
description:
|
||||
- C(onepassword) wraps the C(op) command line utility to fetch specific field values from 1Password.
|
||||
options:
|
||||
_terms:
|
||||
description: identifier(s) (UUID, name, or subdomain; case-insensitive) of item(s) to retrieve.
|
||||
required: True
|
||||
field:
|
||||
description: field to return from each matching item (case-insensitive).
|
||||
default: 'password'
|
||||
master_password:
|
||||
description: The password used to unlock the specified vault.
|
||||
aliases: ['vault_password']
|
||||
section:
|
||||
description: Item section containing the field to retrieve (case-insensitive). If absent will return first match from any section.
|
||||
subdomain:
|
||||
description: The 1Password subdomain to authenticate against.
|
||||
username:
|
||||
description: The username used to sign in.
|
||||
secret_key:
|
||||
description: The secret key used when performing an initial sign in.
|
||||
vault:
|
||||
description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults.
|
||||
notes:
|
||||
- This lookup will use an existing 1Password session if one exists. If not, and you have already
|
||||
performed an initial sign in (meaning C(~/.op/config exists)), then only the C(master_password) is required.
|
||||
You may optionally specify C(subdomain) in this scenario, otherwise the last used subdomain will be used by C(op).
|
||||
- This lookup can perform an initial login by providing C(subdomain), C(username), C(secret_key), and C(master_password).
|
||||
- Due to the B(very) sensitive nature of these credentials, it is B(highly) recommended that you only pass in the minimal credentials
|
||||
needed at any given time. Also, store these credentials in an Ansible Vault using a key that is equal to or greater in strength
|
||||
to the 1Password master password.
|
||||
- This lookup stores potentially sensitive data from 1Password as Ansible facts.
|
||||
Facts are subject to caching if enabled, which means this data could be stored in clear text
|
||||
on disk or in a database.
|
||||
- Tested with C(op) version 0.5.3
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
# These examples only work when already signed in to 1Password
|
||||
- name: Retrieve password for KITT when already signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword', 'KITT')
|
||||
|
||||
- name: Retrieve password for Wintermute when already signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword', 'Tessier-Ashpool', section='Wintermute')
|
||||
|
||||
- name: Retrieve username for HAL when already signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword', 'HAL 9000', field='username', vault='Discovery')
|
||||
|
||||
- name: Retrieve password for HAL when not signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword'
|
||||
'HAL 9000'
|
||||
subdomain='Discovery'
|
||||
master_password=vault_master_password)
|
||||
|
||||
- name: Retrieve password for HAL when never signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword'
|
||||
'HAL 9000'
|
||||
subdomain='Discovery'
|
||||
master_password=vault_master_password
|
||||
username='tweety@acme.com'
|
||||
secret_key=vault_secret_key)
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: field data requested
|
||||
"""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
|
||||
|
||||
class OnePass(object):
|
||||
|
||||
def __init__(self, path='op'):
|
||||
self.cli_path = path
|
||||
self.config_file_path = os.path.expanduser('~/.op/config')
|
||||
self.logged_in = False
|
||||
self.token = None
|
||||
self.subdomain = None
|
||||
self.username = None
|
||||
self.secret_key = None
|
||||
self.master_password = None
|
||||
|
||||
def get_token(self):
|
||||
# If the config file exists, assume an initial signin has taken place and try basic sign in
|
||||
if os.path.isfile(self.config_file_path):
|
||||
|
||||
if not self.master_password:
|
||||
raise AnsibleLookupError('Unable to sign in to 1Password. master_password is required.')
|
||||
|
||||
try:
|
||||
args = ['signin', '--output=raw']
|
||||
|
||||
if self.subdomain:
|
||||
args = ['signin', self.subdomain, '--output=raw']
|
||||
|
||||
rc, out, err = self._run(args, command_input=to_bytes(self.master_password))
|
||||
self.token = out.strip()
|
||||
|
||||
except AnsibleLookupError:
|
||||
self.full_login()
|
||||
|
||||
else:
|
||||
# Attempt a full sign in since there appears to be no existing sign in
|
||||
self.full_login()
|
||||
|
||||
def assert_logged_in(self):
|
||||
try:
|
||||
rc, out, err = self._run(['get', 'account'], ignore_errors=True)
|
||||
if rc == 0:
|
||||
self.logged_in = True
|
||||
if not self.logged_in:
|
||||
self.get_token()
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise AnsibleLookupError("1Password CLI tool '%s' not installed in path on control machine" % self.cli_path)
|
||||
raise e
|
||||
|
||||
def get_raw(self, item_id, vault=None):
|
||||
args = ["get", "item", item_id]
|
||||
if vault is not None:
|
||||
args += ['--vault={0}'.format(vault)]
|
||||
if not self.logged_in:
|
||||
args += [to_bytes('--session=') + self.token]
|
||||
rc, output, dummy = self._run(args)
|
||||
return output
|
||||
|
||||
def get_field(self, item_id, field, section=None, vault=None):
|
||||
output = self.get_raw(item_id, vault)
|
||||
return self._parse_field(output, field, section) if output != '' else ''
|
||||
|
||||
def full_login(self):
|
||||
if None in [self.subdomain, self.username, self.secret_key, self.master_password]:
|
||||
raise AnsibleLookupError('Unable to perform initial sign in to 1Password. '
|
||||
'subdomain, username, secret_key, and master_password are required to perform initial sign in.')
|
||||
|
||||
args = [
|
||||
'signin',
|
||||
'{0}.1password.com'.format(self.subdomain),
|
||||
to_bytes(self.username),
|
||||
to_bytes(self.secret_key),
|
||||
'--output=raw',
|
||||
]
|
||||
|
||||
rc, out, err = self._run(args, command_input=to_bytes(self.master_password))
|
||||
self.token = out.strip()
|
||||
|
||||
def _run(self, args, expected_rc=0, command_input=None, ignore_errors=False):
|
||||
command = [self.cli_path] + args
|
||||
p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
out, err = p.communicate(input=command_input)
|
||||
rc = p.wait()
|
||||
if not ignore_errors and rc != expected_rc:
|
||||
raise AnsibleLookupError(to_text(err))
|
||||
return rc, out, err
|
||||
|
||||
def _parse_field(self, data_json, field_name, section_title=None):
|
||||
data = json.loads(data_json)
|
||||
if section_title is None:
|
||||
for field_data in data['details'].get('fields', []):
|
||||
if field_data.get('name', '').lower() == field_name.lower():
|
||||
return field_data.get('value', '')
|
||||
for section_data in data['details'].get('sections', []):
|
||||
if section_title is not None and section_title.lower() != section_data['title'].lower():
|
||||
continue
|
||||
for field_data in section_data.get('fields', []):
|
||||
if field_data.get('t', '').lower() == field_name.lower():
|
||||
return field_data.get('v', '')
|
||||
return ''
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
op = OnePass()
|
||||
|
||||
field = kwargs.get('field', 'password')
|
||||
section = kwargs.get('section')
|
||||
vault = kwargs.get('vault')
|
||||
op.subdomain = kwargs.get('subdomain')
|
||||
op.username = kwargs.get('username')
|
||||
op.secret_key = kwargs.get('secret_key')
|
||||
op.master_password = kwargs.get('master_password', kwargs.get('vault_password'))
|
||||
|
||||
op.assert_logged_in()
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
values.append(op.get_field(term, field, section, vault))
|
||||
return values
|
94
plugins/lookup/onepassword_raw.py
Normal file
94
plugins/lookup/onepassword_raw.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Scott Buchanan <sbuchanan@ri.pn>
|
||||
# Copyright: (c) 2016, Andrew Zenk <azenk@umn.edu> (lastpass.py used as starting point)
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: onepassword_raw
|
||||
author:
|
||||
- Scott Buchanan (@scottsb)
|
||||
- Andrew Zenk (@azenk)
|
||||
- Sam Doran (@samdoran)
|
||||
requirements:
|
||||
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
|
||||
short_description: fetch an entire item from 1Password
|
||||
description:
|
||||
- C(onepassword_raw) wraps C(op) command line utility to fetch an entire item from 1Password
|
||||
options:
|
||||
_terms:
|
||||
description: identifier(s) (UUID, name, or domain; case-insensitive) of item(s) to retrieve.
|
||||
required: True
|
||||
master_password:
|
||||
description: The password used to unlock the specified vault.
|
||||
aliases: ['vault_password']
|
||||
section:
|
||||
description: Item section containing the field to retrieve (case-insensitive). If absent will return first match from any section.
|
||||
subdomain:
|
||||
description: The 1Password subdomain to authenticate against.
|
||||
username:
|
||||
description: The username used to sign in.
|
||||
secret_key:
|
||||
description: The secret key used when performing an initial sign in.
|
||||
vault:
|
||||
description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults.
|
||||
notes:
|
||||
- This lookup will use an existing 1Password session if one exists. If not, and you have already
|
||||
performed an initial sign in (meaning C(~/.op/config exists)), then only the C(master_password) is required.
|
||||
You may optionally specify C(subdomain) in this scenario, otherwise the last used subdomain will be used by C(op).
|
||||
- This lookup can perform an initial login by providing C(subdomain), C(username), C(secret_key), and C(master_password).
|
||||
- Due to the B(very) sensitive nature of these credentials, it is B(highly) recommended that you only pass in the minimal credentials
|
||||
needed at any given time. Also, store these credentials in an Ansible Vault using a key that is equal to or greater in strength
|
||||
to the 1Password master password.
|
||||
- This lookup stores potentially sensitive data from 1Password as Ansible facts.
|
||||
Facts are subject to caching if enabled, which means this data could be stored in clear text
|
||||
on disk or in a database.
|
||||
- Tested with C(op) version 0.5.3
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Retrieve all data about Wintermute
|
||||
debug:
|
||||
var: lookup('onepassword_raw', 'Wintermute')
|
||||
|
||||
- name: Retrieve all data about Wintermute when not signed in to 1Password
|
||||
debug:
|
||||
var: lookup('onepassword_raw', 'Wintermute', subdomain='Turing', vault_password='DmbslfLvasjdl')
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: field data requested
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from ansible_collections.community.general.plugins.lookup.onepassword import OnePass
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
op = OnePass()
|
||||
|
||||
vault = kwargs.get('vault')
|
||||
op.subdomain = kwargs.get('subdomain')
|
||||
op.username = kwargs.get('username')
|
||||
op.secret_key = kwargs.get('secret_key')
|
||||
op.master_password = kwargs.get('master_password', kwargs.get('vault_password'))
|
||||
|
||||
op.assert_logged_in()
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
data = json.loads(op.get_raw(term, vault))
|
||||
values.append(data)
|
||||
return values
|
280
plugins/lookup/passwordstore.py
Normal file
280
plugins/lookup/passwordstore.py
Normal file
|
@ -0,0 +1,280 @@
|
|||
# (c) 2017, Patrick Deelman <patrick@patrickdeelman.nl>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: passwordstore
|
||||
author:
|
||||
- Patrick Deelman <patrick@patrickdeelman.nl>
|
||||
short_description: manage passwords with passwordstore.org's pass utility
|
||||
description:
|
||||
- Enables Ansible to retrieve, create or update passwords from the passwordstore.org pass utility.
|
||||
It also retrieves YAML style keys stored as multilines in the passwordfile.
|
||||
options:
|
||||
_terms:
|
||||
description: query key
|
||||
required: True
|
||||
passwordstore:
|
||||
description: location of the password store
|
||||
default: '~/.password-store'
|
||||
directory:
|
||||
description: The directory of the password store.
|
||||
env:
|
||||
- name: PASSWORD_STORE_DIR
|
||||
create:
|
||||
description: Create the password if it does not already exist.
|
||||
type: bool
|
||||
default: 'no'
|
||||
overwrite:
|
||||
description: Overwrite the password if it does already exist.
|
||||
type: bool
|
||||
default: 'no'
|
||||
returnall:
|
||||
description: Return all the content of the password, not only the first line.
|
||||
type: bool
|
||||
default: 'no'
|
||||
subkey:
|
||||
description: Return a specific subkey of the password. When set to C(password), always returns the first line.
|
||||
default: password
|
||||
userpass:
|
||||
description: Specify a password to save, instead of a generated one.
|
||||
length:
|
||||
description: The length of the generated password
|
||||
type: integer
|
||||
default: 16
|
||||
backup:
|
||||
description: Used with C(overwrite=yes). Backup the previous password in a subkey.
|
||||
type: bool
|
||||
default: 'no'
|
||||
nosymbols:
|
||||
description: use alphanumeric characters
|
||||
type: bool
|
||||
default: 'no'
|
||||
'''
|
||||
EXAMPLES = """
|
||||
# Debug is used for examples, BAD IDEA to show passwords on screen
|
||||
- name: Basic lookup. Fails if example/test doesn't exist
|
||||
debug:
|
||||
msg: "{{ lookup('passwordstore', 'example/test')}}"
|
||||
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('passwordstore', 'example/test create=true')}}"
|
||||
|
||||
- name: Different size password
|
||||
debug:
|
||||
msg: "{{ lookup('passwordstore', 'example/test create=true length=42')}}"
|
||||
|
||||
- name: Create password and overwrite the password if it exists. As a bonus, this module includes the old password inside the pass file
|
||||
debug:
|
||||
msg: "{{ lookup('passwordstore', 'example/test create=true overwrite=true')}}"
|
||||
|
||||
- name: Create an alphanumeric password
|
||||
debug: msg="{{ lookup('passwordstore', 'example/test create=true nosymbols=true') }}"
|
||||
|
||||
- name: Return the value for user in the KV pair user, username
|
||||
debug:
|
||||
msg: "{{ lookup('passwordstore', 'example/test subkey=user')}}"
|
||||
|
||||
- name: Return the entire password file content
|
||||
set_fact:
|
||||
passfilecontent: "{{ lookup('passwordstore', 'example/test returnall=true')}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- a password
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from distutils import util
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||
from ansible.utils.encrypt import random_password
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible import constants as C
|
||||
|
||||
|
||||
# backhacked check_output with input for python 2.7
|
||||
# http://stackoverflow.com/questions/10103551/passing-data-to-subprocess-check-output
|
||||
def check_output2(*popenargs, **kwargs):
|
||||
if 'stdout' in kwargs:
|
||||
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||
if 'stderr' in kwargs:
|
||||
raise ValueError('stderr argument not allowed, it will be overridden.')
|
||||
if 'input' in kwargs:
|
||||
if 'stdin' in kwargs:
|
||||
raise ValueError('stdin and input arguments may not both be used.')
|
||||
b_inputdata = to_bytes(kwargs['input'], errors='surrogate_or_strict')
|
||||
del kwargs['input']
|
||||
kwargs['stdin'] = subprocess.PIPE
|
||||
else:
|
||||
b_inputdata = None
|
||||
process = subprocess.Popen(*popenargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
|
||||
try:
|
||||
b_out, b_err = process.communicate(b_inputdata)
|
||||
except Exception:
|
||||
process.kill()
|
||||
process.wait()
|
||||
raise
|
||||
retcode = process.poll()
|
||||
if retcode != 0 or \
|
||||
b'encryption failed: Unusable public key' in b_out or \
|
||||
b'encryption failed: Unusable public key' in b_err:
|
||||
cmd = kwargs.get("args")
|
||||
if cmd is None:
|
||||
cmd = popenargs[0]
|
||||
raise subprocess.CalledProcessError(
|
||||
retcode,
|
||||
cmd,
|
||||
to_native(b_out + b_err, errors='surrogate_or_strict')
|
||||
)
|
||||
return b_out
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def parse_params(self, term):
|
||||
# I went with the "traditional" param followed with space separated KV pairs.
|
||||
# Waiting for final implementation of lookup parameter parsing.
|
||||
# See: https://github.com/ansible/ansible/issues/12255
|
||||
params = term.split()
|
||||
if len(params) > 0:
|
||||
# the first param is the pass-name
|
||||
self.passname = params[0]
|
||||
# next parse the optional parameters in keyvalue pairs
|
||||
try:
|
||||
for param in params[1:]:
|
||||
name, value = param.split('=')
|
||||
if name not in self.paramvals:
|
||||
raise AnsibleAssertionError('%s not in paramvals' % name)
|
||||
self.paramvals[name] = value
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
# check and convert values
|
||||
try:
|
||||
for key in ['create', 'returnall', 'overwrite', 'backup', 'nosymbols']:
|
||||
if not isinstance(self.paramvals[key], bool):
|
||||
self.paramvals[key] = util.strtobool(self.paramvals[key])
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
if not isinstance(self.paramvals['length'], int):
|
||||
if self.paramvals['length'].isdigit():
|
||||
self.paramvals['length'] = int(self.paramvals['length'])
|
||||
else:
|
||||
raise AnsibleError("{0} is not a correct value for length".format(self.paramvals['length']))
|
||||
|
||||
# Set PASSWORD_STORE_DIR if directory is set
|
||||
if self.paramvals['directory']:
|
||||
if os.path.isdir(self.paramvals['directory']):
|
||||
os.environ['PASSWORD_STORE_DIR'] = self.paramvals['directory']
|
||||
else:
|
||||
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
|
||||
|
||||
def check_pass(self):
|
||||
try:
|
||||
self.passoutput = to_text(
|
||||
check_output2(["pass", self.passname]),
|
||||
errors='surrogate_or_strict'
|
||||
).splitlines()
|
||||
self.password = self.passoutput[0]
|
||||
self.passdict = {}
|
||||
for line in self.passoutput[1:]:
|
||||
if ':' in line:
|
||||
name, value = line.split(':', 1)
|
||||
self.passdict[name.strip()] = value.strip()
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
if e.returncode == 1 and 'not in the password store' in e.output:
|
||||
# if pass returns 1 and return string contains 'is not in the password store.'
|
||||
# We need to determine if this is valid or Error.
|
||||
if not self.paramvals['create']:
|
||||
raise AnsibleError('passname: {0} not found, use create=True'.format(self.passname))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise AnsibleError(e)
|
||||
return True
|
||||
|
||||
def get_newpass(self):
|
||||
if self.paramvals['nosymbols']:
|
||||
chars = C.DEFAULT_PASSWORD_CHARS[:62]
|
||||
else:
|
||||
chars = C.DEFAULT_PASSWORD_CHARS
|
||||
|
||||
if self.paramvals['userpass']:
|
||||
newpass = self.paramvals['userpass']
|
||||
else:
|
||||
newpass = random_password(length=self.paramvals['length'], chars=chars)
|
||||
return newpass
|
||||
|
||||
def update_password(self):
|
||||
# generate new password, insert old lines from current result and return new password
|
||||
newpass = self.get_newpass()
|
||||
datetime = time.strftime("%d/%m/%Y %H:%M:%S")
|
||||
msg = newpass + '\n'
|
||||
if self.passoutput[1:]:
|
||||
msg += '\n'.join(self.passoutput[1:]) + '\n'
|
||||
if self.paramvals['backup']:
|
||||
msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime)
|
||||
try:
|
||||
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
raise AnsibleError(e)
|
||||
return newpass
|
||||
|
||||
def generate_password(self):
|
||||
# generate new file and insert lookup_pass: Generated by Ansible on {date}
|
||||
# use pwgen to generate the password and insert values with pass -m
|
||||
newpass = self.get_newpass()
|
||||
datetime = time.strftime("%d/%m/%Y %H:%M:%S")
|
||||
msg = newpass + '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime)
|
||||
try:
|
||||
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
raise AnsibleError(e)
|
||||
return newpass
|
||||
|
||||
def get_passresult(self):
|
||||
if self.paramvals['returnall']:
|
||||
return os.linesep.join(self.passoutput)
|
||||
if self.paramvals['subkey'] == 'password':
|
||||
return self.password
|
||||
else:
|
||||
if self.paramvals['subkey'] in self.passdict:
|
||||
return self.passdict[self.paramvals['subkey']]
|
||||
else:
|
||||
return None
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
result = []
|
||||
self.paramvals = {
|
||||
'subkey': 'password',
|
||||
'directory': variables.get('passwordstore'),
|
||||
'create': False,
|
||||
'returnall': False,
|
||||
'overwrite': False,
|
||||
'nosymbols': False,
|
||||
'userpass': '',
|
||||
'length': 16,
|
||||
'backup': False,
|
||||
}
|
||||
|
||||
for term in terms:
|
||||
self.parse_params(term) # parse the input into paramvals
|
||||
if self.check_pass(): # password exists
|
||||
if self.paramvals['overwrite'] and self.paramvals['subkey'] == 'password':
|
||||
result.append(self.update_password())
|
||||
else:
|
||||
result.append(self.get_passresult())
|
||||
else: # password does not exist
|
||||
if self.paramvals['create']:
|
||||
result.append(self.generate_password())
|
||||
return result
|
189
plugins/lookup/rabbitmq.py
Normal file
189
plugins/lookup/rabbitmq.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
# (c) 2018, John Imison <john+github@imison.net>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: rabbitmq
|
||||
author: John Imison <@Im0>
|
||||
short_description: Retrieve messages from an AMQP/AMQPS RabbitMQ queue.
|
||||
description:
|
||||
- This lookup uses a basic get to retrieve all, or a limited number C(count), messages from a RabbitMQ queue.
|
||||
options:
|
||||
url:
|
||||
description:
|
||||
- An URI connection string to connect to the AMQP/AMQPS RabbitMQ server.
|
||||
- For more information refer to the URI spec U(https://www.rabbitmq.com/uri-spec.html).
|
||||
required: True
|
||||
queue:
|
||||
description:
|
||||
- The queue to get messages from.
|
||||
required: True
|
||||
count:
|
||||
description:
|
||||
- How many messages to collect from the queue.
|
||||
- If not set, defaults to retrieving all the messages from the queue.
|
||||
requirements:
|
||||
- The python pika package U(https://pypi.org/project/pika/).
|
||||
notes:
|
||||
- This lookup implements BlockingChannel.basic_get to get messages from a RabbitMQ server.
|
||||
- After retrieving a message from the server, receipt of the message is acknowledged and the message on the server is deleted.
|
||||
- Pika is a pure-Python implementation of the AMQP 0-9-1 protocol that tries to stay fairly independent of the underlying network support library.
|
||||
- More information about pika can be found at U(https://pika.readthedocs.io/en/stable/).
|
||||
- This plugin is tested against RabbitMQ. Other AMQP 0.9.1 protocol based servers may work but not tested/guaranteed.
|
||||
- Assigning the return messages to a variable under C(vars) may result in unexpected results as the lookup is evaluated every time the
|
||||
variable is referenced.
|
||||
- Currently this plugin only handles text based messages from a queue. Unexpected results may occur when retrieving binary data.
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Get all messages off a queue
|
||||
debug:
|
||||
msg: "{{ lookup('rabbitmq', url='amqp://guest:guest@192.168.0.10:5672/%2F', queue='hello') }}"
|
||||
|
||||
|
||||
# If you are intending on using the returned messages as a variable in more than
|
||||
# one task (eg. debug, template), it is recommended to set_fact.
|
||||
|
||||
- name: Get 2 messages off a queue and set a fact for re-use
|
||||
set_fact:
|
||||
messages: "{{ lookup('rabbitmq', url='amqp://guest:guest@192.168.0.10:5672/%2F', queue='hello', count=2) }}"
|
||||
|
||||
- name: Dump out contents of the messages
|
||||
debug:
|
||||
var: messages
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- A list of dictionaries with keys and value from the queue.
|
||||
type: list
|
||||
contains:
|
||||
content_type:
|
||||
description: The content_type on the message in the queue.
|
||||
type: str
|
||||
delivery_mode:
|
||||
description: The delivery_mode on the message in the queue.
|
||||
type: str
|
||||
delivery_tag:
|
||||
description: The delivery_tag on the message in the queue.
|
||||
type: str
|
||||
exchange:
|
||||
description: The exchange the message came from.
|
||||
type: str
|
||||
message_count:
|
||||
description: The message_count for the message on the queue.
|
||||
type: str
|
||||
msg:
|
||||
description: The content of the message.
|
||||
type: str
|
||||
redelivered:
|
||||
description: The redelivered flag. True if the message has been delivered before.
|
||||
type: bool
|
||||
routing_key:
|
||||
description: The routing_key on the message in the queue.
|
||||
type: str
|
||||
headers:
|
||||
description: The headers for the message returned from the queue.
|
||||
type: dict
|
||||
json:
|
||||
description: If application/json is specified in content_type, json will be loaded into variables.
|
||||
type: dict
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
from ansible.utils.display import Display
|
||||
|
||||
try:
|
||||
import pika
|
||||
from pika import spec
|
||||
HAS_PIKA = True
|
||||
except ImportError:
|
||||
HAS_PIKA = False
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, url=None, queue=None, count=None):
|
||||
if not HAS_PIKA:
|
||||
raise AnsibleError('pika python package is required for rabbitmq lookup.')
|
||||
if not url:
|
||||
raise AnsibleError('URL is required for rabbitmq lookup.')
|
||||
if not queue:
|
||||
raise AnsibleError('Queue is required for rabbitmq lookup.')
|
||||
|
||||
display.vvv(u"terms:%s : variables:%s url:%s queue:%s count:%s" % (terms, variables, url, queue, count))
|
||||
|
||||
try:
|
||||
parameters = pika.URLParameters(url)
|
||||
except Exception as e:
|
||||
raise AnsibleError("URL malformed: %s" % to_native(e))
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(parameters)
|
||||
except Exception as e:
|
||||
raise AnsibleError("Connection issue: %s" % to_native(e))
|
||||
|
||||
try:
|
||||
conn_channel = connection.channel()
|
||||
except pika.exceptions.AMQPChannelError as e:
|
||||
try:
|
||||
connection.close()
|
||||
except pika.exceptions.AMQPConnectionError as ie:
|
||||
raise AnsibleError("Channel and connection closing issues: %s / %s" % to_native(e), to_native(ie))
|
||||
raise AnsibleError("Channel issue: %s" % to_native(e))
|
||||
|
||||
ret = []
|
||||
idx = 0
|
||||
|
||||
while True:
|
||||
method_frame, properties, body = conn_channel.basic_get(queue=queue)
|
||||
if method_frame:
|
||||
display.vvv(u"%s, %s, %s " % (method_frame, properties, to_text(body)))
|
||||
|
||||
# TODO: In the future consider checking content_type and handle text/binary data differently.
|
||||
msg_details = dict({
|
||||
'msg': to_text(body),
|
||||
'message_count': method_frame.message_count,
|
||||
'routing_key': method_frame.routing_key,
|
||||
'delivery_tag': method_frame.delivery_tag,
|
||||
'redelivered': method_frame.redelivered,
|
||||
'exchange': method_frame.exchange,
|
||||
'delivery_mode': properties.delivery_mode,
|
||||
'content_type': properties.content_type,
|
||||
'headers': properties.headers
|
||||
})
|
||||
if properties.content_type == 'application/json':
|
||||
try:
|
||||
msg_details['json'] = json.loads(msg_details['msg'])
|
||||
except ValueError as e:
|
||||
raise AnsibleError("Unable to decode JSON for message %s: %s" % (method_frame.delivery_tag, to_native(e)))
|
||||
|
||||
ret.append(msg_details)
|
||||
conn_channel.basic_ack(method_frame.delivery_tag)
|
||||
idx += 1
|
||||
if method_frame.message_count == 0 or idx == count:
|
||||
break
|
||||
# If we didn't get a method_frame, exit.
|
||||
else:
|
||||
break
|
||||
|
||||
if connection.is_closed:
|
||||
return [ret]
|
||||
else:
|
||||
try:
|
||||
connection.close()
|
||||
except pika.exceptions.AMQPConnectionError:
|
||||
pass
|
||||
return [ret]
|
111
plugins/lookup/redis.py
Normal file
111
plugins/lookup/redis.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
# (c) 2012, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: redis
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com>
|
||||
- Ansible Core
|
||||
short_description: fetch data from Redis
|
||||
description:
|
||||
- This lookup returns a list of results from a Redis DB corresponding to a list of items given to it
|
||||
requirements:
|
||||
- redis (python library https://github.com/andymccurdy/redis-py/)
|
||||
options:
|
||||
_terms:
|
||||
description: list of keys to query
|
||||
host:
|
||||
description: location of Redis host
|
||||
default: '127.0.0.1'
|
||||
env:
|
||||
- name: ANSIBLE_REDIS_HOST
|
||||
ini:
|
||||
- section: lookup_redis
|
||||
key: host
|
||||
port:
|
||||
description: port on which Redis is listening on
|
||||
default: 6379
|
||||
type: int
|
||||
env:
|
||||
- name: ANSIBLE_REDIS_PORT
|
||||
ini:
|
||||
- section: lookup_redis
|
||||
key: port
|
||||
socket:
|
||||
description: path to socket on which to query Redis, this option overrides host and port options when set.
|
||||
type: path
|
||||
env:
|
||||
- name: ANSIBLE_REDIS_SOCKET
|
||||
ini:
|
||||
- section: lookup_redis
|
||||
key: socket
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: query redis for somekey (default or configured settings used)
|
||||
debug: msg="{{ lookup('redis', 'somekey') }}"
|
||||
|
||||
- name: query redis for list of keys and non-default host and port
|
||||
debug: msg="{{ lookup('redis', item, host='myredis.internal.com', port=2121) }}"
|
||||
loop: '{{list_of_redis_keys}}'
|
||||
|
||||
- name: use list directly
|
||||
debug: msg="{{ lookup('redis', 'key1', 'key2', 'key3') }}"
|
||||
|
||||
- name: use list directly with a socket
|
||||
debug: msg="{{ lookup('redis', 'key1', 'key2', socket='/var/tmp/redis.sock') }}"
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: value(s) stored in Redis
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
HAVE_REDIS = False
|
||||
try:
|
||||
import redis
|
||||
HAVE_REDIS = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not HAVE_REDIS:
|
||||
raise AnsibleError("Can't LOOKUP(redis_kv): module redis is not installed")
|
||||
|
||||
# get options
|
||||
self.set_options(direct=kwargs)
|
||||
|
||||
# setup connection
|
||||
host = self.get_option('host')
|
||||
port = self.get_option('port')
|
||||
socket = self.get_option('socket')
|
||||
if socket is None:
|
||||
conn = redis.Redis(host=host, port=port)
|
||||
else:
|
||||
conn = redis.Redis(unix_socket_path=socket)
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
try:
|
||||
res = conn.get(term)
|
||||
if res is None:
|
||||
res = ""
|
||||
ret.append(to_text(res))
|
||||
except Exception as e:
|
||||
# connection failed or key not found
|
||||
raise AnsibleError('Encountered exception while fetching {0}: {1}'.format(term, e))
|
||||
return ret
|
88
plugins/lookup/shelvefile.py
Normal file
88
plugins/lookup/shelvefile.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# (c) 2015, Alejandro Guirao <lekumberri@gmail.com>
|
||||
# (c) 2012-17 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: shelvefile
|
||||
author: Alejandro Guirao <lekumberri@gmail.com>
|
||||
short_description: read keys from Python shelve file
|
||||
description:
|
||||
- Read keys from Python shelve file.
|
||||
options:
|
||||
_terms:
|
||||
description: sets of key value pairs of parameters
|
||||
key:
|
||||
description: key to query
|
||||
required: True
|
||||
file:
|
||||
description: path to shelve file
|
||||
required: True
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: retrieve a string value corresponding to a key inside a Python shelve file
|
||||
debug: msg="{{ lookup('shelvefile', 'file=path_to_some_shelve_file.db key=key_to_retrieve') }}
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description: value(s) of key(s) in shelve file(s)
|
||||
"""
|
||||
import shelve
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def read_shelve(self, shelve_filename, key):
|
||||
"""
|
||||
Read the value of "key" from a shelve file
|
||||
"""
|
||||
d = shelve.open(to_bytes(shelve_filename))
|
||||
res = d.get(key, None)
|
||||
d.close()
|
||||
return res
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [terms]
|
||||
|
||||
ret = []
|
||||
|
||||
for term in terms:
|
||||
paramvals = {"file": None, "key": None}
|
||||
params = term.split()
|
||||
|
||||
try:
|
||||
for param in params:
|
||||
name, value = param.split('=')
|
||||
if name not in paramvals:
|
||||
raise AnsibleAssertionError('%s not in paramvals' % name)
|
||||
paramvals[name] = value
|
||||
|
||||
except (ValueError, AssertionError) as e:
|
||||
# In case "file" or "key" are not present
|
||||
raise AnsibleError(e)
|
||||
|
||||
key = paramvals['key']
|
||||
|
||||
# Search also in the role/files directory and in the playbook directory
|
||||
shelvefile = self.find_file_in_search_path(variables, 'files', paramvals['file'])
|
||||
|
||||
if shelvefile:
|
||||
res = self.read_shelve(shelvefile, key)
|
||||
if res is None:
|
||||
raise AnsibleError("Key %s not found in shelve file %s" % (key, shelvefile))
|
||||
# Convert the value read to string
|
||||
ret.append(to_text(res))
|
||||
break
|
||||
else:
|
||||
raise AnsibleError("Could not locate shelve file in lookup: %s" % paramvals['file'])
|
||||
|
||||
return ret
|
Loading…
Add table
Add a link
Reference in a new issue