Initial project commit
This commit is contained in:
@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022, Jonathan Lung <lungj@heresjono.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
name: bitwarden
|
||||
author:
|
||||
- Jonathan Lung (@lungj) <lungj@heresjono.com>
|
||||
requirements:
|
||||
- bw (command line utility)
|
||||
- be logged into bitwarden
|
||||
- bitwarden vault unlocked
|
||||
- E(BW_SESSION) environment variable set
|
||||
short_description: Retrieve secrets from Bitwarden
|
||||
version_added: 5.4.0
|
||||
description:
|
||||
- Retrieve secrets from Bitwarden.
|
||||
options:
|
||||
_terms:
|
||||
description: Key(s) to fetch values for from login info.
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
search:
|
||||
description:
|
||||
- Field to retrieve, for example V(name) or V(id).
|
||||
- If set to V(id), only zero or one element can be returned.
|
||||
Use the Jinja C(first) filter to get the only list element.
|
||||
- If set to V(None) or V(''), or if O(_terms) is empty, records are not filtered by fields.
|
||||
type: str
|
||||
default: name
|
||||
version_added: 5.7.0
|
||||
field:
|
||||
description: Field to fetch. Leave unset to fetch whole response.
|
||||
type: str
|
||||
collection_id:
|
||||
description: Collection ID to filter results by collection. Leave unset to skip filtering.
|
||||
type: str
|
||||
version_added: 6.3.0
|
||||
organization_id:
|
||||
description: Organization ID to filter results by organization. Leave unset to skip filtering.
|
||||
type: str
|
||||
version_added: 8.5.0
|
||||
bw_session:
|
||||
description: Pass session key instead of reading from env.
|
||||
type: str
|
||||
version_added: 8.4.0
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: "Get 'password' from all Bitwarden records named 'a_test'"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='password') }}
|
||||
|
||||
- name: "Get 'password' from Bitwarden record with ID 'bafba515-af11-47e6-abe3-af1200cd18b2'"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') | first }}
|
||||
|
||||
- name: "Get 'password' from all Bitwarden records named 'a_test' from collection"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='password', collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}
|
||||
|
||||
- name: "Get list of all full Bitwarden records named 'a_test'"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test') }}
|
||||
|
||||
- name: "Get custom field 'api_key' from all Bitwarden records named 'a_test'"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='api_key') }}
|
||||
|
||||
- name: "Get 'password' from all Bitwarden records named 'a_test', using given session key"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='password', bw_session='bXZ9B5TXi6...') }}
|
||||
|
||||
- name: "Get all Bitwarden records from collection"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', None, collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- A one-element list that contains a list of requested fields or JSON objects of matches.
|
||||
- If you use C(query), you get a list of lists. If you use C(lookup) without C(wantlist=true),
|
||||
this always gets reduced to a list of field values or JSON objects.
|
||||
type: list
|
||||
elements: list
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
from ansible.parsing.ajson import AnsibleJSONDecoder
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class BitwardenException(AnsibleError):
|
||||
pass
|
||||
|
||||
|
||||
class Bitwarden(object):
|
||||
|
||||
def __init__(self, path='bw'):
|
||||
self._cli_path = path
|
||||
self._session = None
|
||||
|
||||
@property
|
||||
def cli_path(self):
|
||||
return self._cli_path
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
@session.setter
|
||||
def session(self, value):
|
||||
self._session = value
|
||||
|
||||
@property
|
||||
def unlocked(self):
|
||||
out, err = self._run(['status'], stdin="")
|
||||
decoded = AnsibleJSONDecoder().raw_decode(out)[0]
|
||||
return decoded['status'] == 'unlocked'
|
||||
|
||||
def _run(self, args, stdin=None, expected_rc=0):
|
||||
if self.session:
|
||||
args += ['--session', self.session]
|
||||
|
||||
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:
|
||||
if len(args) > 2 and args[0] == 'get' and args[1] == 'item' and b'Not found.' in err:
|
||||
return 'null', ''
|
||||
raise BitwardenException(err)
|
||||
return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')
|
||||
|
||||
def _get_matches(self, search_value, search_field, collection_id=None, organization_id=None):
|
||||
"""Return matching records whose search_field is equal to key.
|
||||
"""
|
||||
|
||||
# Prepare set of params for Bitwarden CLI
|
||||
if search_field == 'id':
|
||||
params = ['get', 'item', search_value]
|
||||
else:
|
||||
params = ['list', 'items']
|
||||
if search_value:
|
||||
params.extend(['--search', search_value])
|
||||
|
||||
if collection_id:
|
||||
params.extend(['--collectionid', collection_id])
|
||||
if organization_id:
|
||||
params.extend(['--organizationid', organization_id])
|
||||
|
||||
out, err = self._run(params)
|
||||
|
||||
# This includes things that matched in different fields.
|
||||
initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]
|
||||
|
||||
if search_field == 'id':
|
||||
if initial_matches is None:
|
||||
initial_matches = []
|
||||
else:
|
||||
initial_matches = [initial_matches]
|
||||
|
||||
# Filter to only include results from the right field, if a search is requested by value or field
|
||||
return [item for item in initial_matches
|
||||
if not search_value or not search_field or item.get(search_field) == search_value]
|
||||
|
||||
def get_field(self, field, search_value, search_field="name", collection_id=None, organization_id=None):
|
||||
"""Return a list of the specified field for records whose search_field match search_value
|
||||
and filtered by collection if collection has been provided.
|
||||
|
||||
If field is None, return the whole record for each match.
|
||||
"""
|
||||
matches = self._get_matches(search_value, search_field, collection_id, organization_id)
|
||||
if not field:
|
||||
return matches
|
||||
field_matches = []
|
||||
for match in matches:
|
||||
# if there are no custom fields, then `match` has no key 'fields'
|
||||
if 'fields' in match:
|
||||
custom_field_found = False
|
||||
for custom_field in match['fields']:
|
||||
if field == custom_field['name']:
|
||||
field_matches.append(custom_field['value'])
|
||||
custom_field_found = True
|
||||
break
|
||||
if custom_field_found:
|
||||
continue
|
||||
if 'login' in match and field in match['login']:
|
||||
field_matches.append(match['login'][field])
|
||||
continue
|
||||
if field in match:
|
||||
field_matches.append(match[field])
|
||||
continue
|
||||
|
||||
if matches and not field_matches:
|
||||
raise AnsibleError("field {field} does not exist in {search_value}".format(field=field, search_value=search_value))
|
||||
|
||||
return field_matches
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms=None, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
field = self.get_option('field')
|
||||
search_field = self.get_option('search')
|
||||
collection_id = self.get_option('collection_id')
|
||||
organization_id = self.get_option('organization_id')
|
||||
_bitwarden.session = self.get_option('bw_session')
|
||||
|
||||
if not _bitwarden.unlocked:
|
||||
raise AnsibleError("Bitwarden Vault locked. Run 'bw unlock'.")
|
||||
|
||||
if not terms:
|
||||
terms = [None]
|
||||
|
||||
return [_bitwarden.get_field(field, term, search_field, collection_id, organization_id) for term in terms]
|
||||
|
||||
|
||||
_bitwarden = Bitwarden()
|
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023, jantari (https://github.com/jantari)
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
name: bitwarden_secrets_manager
|
||||
author:
|
||||
- jantari (@jantari)
|
||||
requirements:
|
||||
- bws (command line utility)
|
||||
short_description: Retrieve secrets from Bitwarden Secrets Manager
|
||||
version_added: 7.2.0
|
||||
description:
|
||||
- Retrieve secrets from Bitwarden Secrets Manager.
|
||||
options:
|
||||
_terms:
|
||||
description: Secret ID(s) to fetch values for.
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
bws_access_token:
|
||||
description: The BWS access token to use for this lookup.
|
||||
env:
|
||||
- name: BWS_ACCESS_TOKEN
|
||||
required: true
|
||||
type: str
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Get a secret relying on the BWS_ACCESS_TOKEN environment variable for authentication
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup("community.general.bitwarden_secrets_manager", "2bc23e48-4932-40de-a047-5524b7ddc972") }}
|
||||
|
||||
- name: Get a secret passing an explicit access token for authentication
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{
|
||||
lookup(
|
||||
"community.general.bitwarden_secrets_manager",
|
||||
"2bc23e48-4932-40de-a047-5524b7ddc972",
|
||||
bws_access_token="9.4f570d14-4b54-42f5-bc07-60f4450b1db5.YmluYXJ5LXNvbWV0aGluZy0xMjMK:d2h5IGhlbGxvIHRoZXJlCg=="
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Get two different secrets each using a different access token for authentication
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- '{{ lookup("community.general.bitwarden_secrets_manager", "2bc23e48-4932-40de-a047-5524b7ddc972", bws_access_token=token1) }}'
|
||||
- '{{ lookup("community.general.bitwarden_secrets_manager", "9d89af4c-eb5d-41f5-bb0f-4ae81215c768", bws_access_token=token2) }}'
|
||||
vars:
|
||||
token1: "9.4f570d14-4b54-42f5-bc07-60f4450b1db5.YmluYXJ5LXNvbWV0aGluZy0xMjMK:d2h5IGhlbGxvIHRoZXJlCg=="
|
||||
token2: "1.69b72797-6ea9-4687-a11e-848e41a30ae6.YW5zaWJsZSBpcyBncmVhdD8K:YW5zaWJsZSBpcyBncmVhdAo="
|
||||
|
||||
- name: Get just the value of a secret
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup("community.general.bitwarden_secrets_manager", "2bc23e48-4932-40de-a047-5524b7ddc972").value }}
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: List containing one or more secrets.
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from time import sleep
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.parsing.ajson import AnsibleJSONDecoder
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class BitwardenSecretsManagerException(AnsibleLookupError):
|
||||
pass
|
||||
|
||||
|
||||
class BitwardenSecretsManager(object):
|
||||
def __init__(self, path='bws'):
|
||||
self._cli_path = path
|
||||
self._max_retries = 3
|
||||
self._retry_delay = 1
|
||||
|
||||
@property
|
||||
def cli_path(self):
|
||||
return self._cli_path
|
||||
|
||||
def _run_with_retry(self, args, stdin=None, retries=0):
|
||||
out, err, rc = self._run(args, stdin)
|
||||
|
||||
if rc != 0:
|
||||
if retries >= self._max_retries:
|
||||
raise BitwardenSecretsManagerException("Max retries exceeded. Unable to retrieve secret.")
|
||||
|
||||
if "Too many requests" in err:
|
||||
delay = self._retry_delay * (2 ** retries)
|
||||
sleep(delay)
|
||||
return self._run_with_retry(args, stdin, retries + 1)
|
||||
else:
|
||||
raise BitwardenSecretsManagerException(f"Command failed with return code {rc}: {err}")
|
||||
|
||||
return out, err, rc
|
||||
|
||||
def _run(self, args, stdin=None):
|
||||
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
out, err = p.communicate(stdin)
|
||||
rc = p.wait()
|
||||
return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict'), rc
|
||||
|
||||
def get_secret(self, secret_id, bws_access_token):
|
||||
"""Get and return the secret with the given secret_id.
|
||||
"""
|
||||
|
||||
# Prepare set of params for Bitwarden Secrets Manager CLI
|
||||
# Color output was not always disabled correctly with the default 'auto' setting so explicitly disable it.
|
||||
params = [
|
||||
'--color', 'no',
|
||||
'--access-token', bws_access_token,
|
||||
'get', 'secret', secret_id
|
||||
]
|
||||
|
||||
out, err, rc = self._run_with_retry(params)
|
||||
if rc != 0:
|
||||
raise BitwardenSecretsManagerException(to_text(err))
|
||||
|
||||
return AnsibleJSONDecoder().raw_decode(out)[0]
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
bws_access_token = self.get_option('bws_access_token')
|
||||
|
||||
return [_bitwarden_secrets_manager.get_secret(term, bws_access_token) for term in terms]
|
||||
|
||||
|
||||
_bitwarden_secrets_manager = BitwardenSecretsManager()
|
@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013, Bradley Young <young.bradley@gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: 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:
|
||||
_terms:
|
||||
description:
|
||||
- a set of lists
|
||||
type: list
|
||||
elements: list
|
||||
required: true
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Example of the change in the description
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.cartesian', [1,2,3], [a, b])}}"
|
||||
|
||||
- name: loops over the cartesian product of the supplied lists
|
||||
ansible.builtin.debug:
|
||||
msg: "{{item}}"
|
||||
with_community.general.cartesian:
|
||||
- "{{list1}}"
|
||||
- "{{list2}}"
|
||||
- [1,2,3,4,5,6]
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- list of lists composed of elements of the input lists
|
||||
type: list
|
||||
elements: list
|
||||
"""
|
||||
|
||||
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:
|
||||
try:
|
||||
intermediate = listify_lookup_plugin_terms(x, templar=self._templar)
|
||||
except TypeError:
|
||||
# The loader argument is deprecated in ansible-core 2.14+. Fall back to
|
||||
# pre-2.14 behavior for older ansible-core versions.
|
||||
intermediate = listify_lookup_plugin_terms(x, templar=self._templar, loader=self._loader)
|
||||
results.append(intermediate)
|
||||
return results
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=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)]
|
@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016, Josh Bradley <jbradley(at)digitalocean.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: 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 (L(Python library, https://pychef.readthedocs.io), C(pip install pychef))"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the databag
|
||||
required: true
|
||||
item:
|
||||
description:
|
||||
- Item to fetch
|
||||
required: true
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.chef_databag', 'name=data_bag_name item=data_bag_item') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- The value from the databag.
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
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" % list(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)]
|
@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
name: collection_version
|
||||
author: Felix Fontein (@felixfontein)
|
||||
version_added: "4.0.0"
|
||||
short_description: Retrieves the version of an installed collection
|
||||
description:
|
||||
- This lookup allows to query the version of an installed collection, and to determine whether a
|
||||
collection is installed at all.
|
||||
- By default it returns V(none) for non-existing collections and V(*) for collections without a
|
||||
version number. The latter should only happen in development environments, or when installing
|
||||
a collection from git which has no version in its C(galaxy.yml). This behavior can be adjusted
|
||||
by providing other values with O(result_not_found) and O(result_no_version).
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- The collections to look for.
|
||||
- For example V(community.general).
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
result_not_found:
|
||||
description:
|
||||
- The value to return when the collection could not be found.
|
||||
- By default, V(none) is returned.
|
||||
type: string
|
||||
default: ~
|
||||
result_no_version:
|
||||
description:
|
||||
- The value to return when the collection has no version number.
|
||||
- This can happen for collections installed from git which do not have a version number
|
||||
in C(galaxy.yml).
|
||||
- By default, V(*) is returned.
|
||||
type: string
|
||||
default: '*'
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Check version of community.general
|
||||
ansible.builtin.debug:
|
||||
msg: "community.general version {{ lookup('community.general.collection_version', 'community.general') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- The version number of the collections listed as input.
|
||||
- If a collection can not be found, it will return the value provided in O(result_not_found).
|
||||
By default, this is V(none).
|
||||
- If a collection can be found, but the version not identified, it will return the value provided in
|
||||
O(result_no_version). By default, this is V(*). This can happen for collections installed
|
||||
from git which do not have a version number in V(galaxy.yml).
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils.compat.importlib import import_module
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
FQCN_RE = re.compile(r'^[A-Za-z0-9_]+\.[A-Za-z0-9_]+$')
|
||||
|
||||
|
||||
def load_collection_meta_manifest(manifest_path):
|
||||
with open(manifest_path, 'rb') as f:
|
||||
meta = json.load(f)
|
||||
return {
|
||||
'version': meta['collection_info']['version'],
|
||||
}
|
||||
|
||||
|
||||
def load_collection_meta_galaxy(galaxy_path, no_version='*'):
|
||||
with open(galaxy_path, 'rb') as f:
|
||||
meta = yaml.safe_load(f)
|
||||
return {
|
||||
'version': meta.get('version') or no_version,
|
||||
}
|
||||
|
||||
|
||||
def load_collection_meta(collection_pkg, no_version='*'):
|
||||
path = os.path.dirname(collection_pkg.__file__)
|
||||
|
||||
# Try to load MANIFEST.json
|
||||
manifest_path = os.path.join(path, 'MANIFEST.json')
|
||||
if os.path.exists(manifest_path):
|
||||
return load_collection_meta_manifest(manifest_path)
|
||||
|
||||
# Try to load galaxy.yml
|
||||
galaxy_path = os.path.join(path, 'galaxy.yml')
|
||||
if os.path.exists(galaxy_path):
|
||||
return load_collection_meta_galaxy(galaxy_path, no_version=no_version)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
result = []
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
not_found = self.get_option('result_not_found')
|
||||
no_version = self.get_option('result_no_version')
|
||||
|
||||
for term in terms:
|
||||
if not FQCN_RE.match(term):
|
||||
raise AnsibleLookupError('"{term}" is not a FQCN'.format(term=term))
|
||||
|
||||
try:
|
||||
collection_pkg = import_module('ansible_collections.{fqcn}'.format(fqcn=term))
|
||||
except ImportError:
|
||||
# Collection not found
|
||||
result.append(not_found)
|
||||
continue
|
||||
|
||||
try:
|
||||
data = load_collection_meta(collection_pkg, no_version=no_version)
|
||||
except Exception as exc:
|
||||
raise AnsibleLookupError('Error while loading metadata for {fqcn}: {error}'.format(fqcn=term, error=exc))
|
||||
|
||||
result.append(data.get('version', no_version))
|
||||
|
||||
return result
|
@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: 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
|
||||
elements: string
|
||||
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 datacenter 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 E(ANSIBLE_CONSUL_URL) if that is set.
|
||||
ini:
|
||||
- section: lookup_consul
|
||||
key: host
|
||||
port:
|
||||
description:
|
||||
- The port of the target host to connect to.
|
||||
- If you use E(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 E(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
|
||||
url:
|
||||
description:
|
||||
- The target to connect to.
|
||||
- "Should look like this: V(https://my.consul.server:8500)."
|
||||
type: str
|
||||
version_added: 1.0.0
|
||||
env:
|
||||
- name: ANSIBLE_CONSUL_URL
|
||||
ini:
|
||||
- section: lookup_consul
|
||||
key: url
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- ansible.builtin.debug:
|
||||
msg: 'key contains {{item}}'
|
||||
with_community.general.consul_kv:
|
||||
- 'key/to/retrieve'
|
||||
|
||||
- name: Parameters can be provided after the key be more specific about what to retrieve
|
||||
ansible.builtin.debug:
|
||||
msg: 'key contains {{item}}'
|
||||
with_community.general.consul_kv:
|
||||
- 'key/to recurse=true token=E6C060A9-26FB-407A-B83E-12DDAFCB4D98'
|
||||
|
||||
- name: retrieving a KV from a remote cluster on non default port
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.consul_kv', 'my/key', host='10.10.10.10', port='2000') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- Value(s) stored in consul.
|
||||
type: dict
|
||||
"""
|
||||
|
||||
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.common.text.converters 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')
|
||||
|
||||
# get options
|
||||
self.set_options(direct=kwargs)
|
||||
|
||||
scheme = self.get_option('scheme')
|
||||
host = self.get_option('host')
|
||||
port = self.get_option('port')
|
||||
url = self.get_option('url')
|
||||
if url is not None:
|
||||
u = urlparse(url)
|
||||
if u.scheme:
|
||||
scheme = u.scheme
|
||||
host = u.hostname
|
||||
if u.port is not None:
|
||||
port = u.port
|
||||
|
||||
validate_certs = self.get_option('validate_certs')
|
||||
client_cert = self.get_option('client_cert')
|
||||
|
||||
values = []
|
||||
try:
|
||||
for term in terms:
|
||||
params = self.parse_params(term)
|
||||
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': self.get_option('token'),
|
||||
'recurse': self.get_option('recurse'),
|
||||
'index': self.get_option('index'),
|
||||
'datacenter': self.get_option('datacenter')
|
||||
}
|
||||
|
||||
# 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
|
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Ensighten <infra@ensighten.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: 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
|
||||
elements: string
|
||||
required: true
|
||||
table:
|
||||
description: name of the credstash table to query
|
||||
type: str
|
||||
default: 'credential-store'
|
||||
version:
|
||||
description: Credstash version
|
||||
type: str
|
||||
default: ''
|
||||
region:
|
||||
description: AWS region
|
||||
type: str
|
||||
profile_name:
|
||||
description: AWS profile to use for authentication
|
||||
type: str
|
||||
env:
|
||||
- name: AWS_PROFILE
|
||||
aws_access_key_id:
|
||||
description: AWS access key ID
|
||||
type: str
|
||||
env:
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
aws_secret_access_key:
|
||||
description: AWS access key
|
||||
type: str
|
||||
env:
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
aws_session_token:
|
||||
description: AWS session token
|
||||
type: str
|
||||
env:
|
||||
- name: AWS_SESSION_TOKEN
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: first use credstash to store your secrets
|
||||
ansible.builtin.shell: credstash put my-github-password secure123
|
||||
|
||||
- name: "Test credstash lookup plugin -- get my github password"
|
||||
ansible.builtin.debug:
|
||||
msg: "Credstash lookup! {{ lookup('community.general.credstash', 'my-github-password') }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get my other password from us-west-1"
|
||||
ansible.builtin.debug:
|
||||
msg: "Credstash lookup! {{ lookup('community.general.credstash', 'my-other-password', region='us-west-1') }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the company's github password"
|
||||
ansible.builtin.debug:
|
||||
msg: "Credstash lookup! {{ lookup('community.general.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"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.credstash', 'some-password', context=context) }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the password with a context defined here"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.credstash', 'some-password', context=dict(app='my_app', environment='production')) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- Value(s) stored in Credstash.
|
||||
type: str
|
||||
"""
|
||||
|
||||
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=None, **kwargs):
|
||||
if not CREDSTASH_INSTALLED:
|
||||
raise AnsibleError('The credstash lookup plugin requires credstash to be installed.')
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
version = self.get_option('version')
|
||||
region = self.get_option('region')
|
||||
table = self.get_option('table')
|
||||
profile_name = self.get_option('profile_name')
|
||||
aws_access_key_id = self.get_option('aws_access_key_id')
|
||||
aws_secret_access_key = self.get_option('aws_secret_access_key')
|
||||
aws_session_token = self.get_option('aws_session_token')
|
||||
|
||||
context = dict(
|
||||
(k, v) for k, v in kwargs.items()
|
||||
if k not in ('version', 'region', 'table', 'profile_name', 'aws_access_key_id', 'aws_secret_access_key', 'aws_session_token')
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
try:
|
||||
ret.append(credstash.getSecret(term, version, region, table, context=context, **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))
|
||||
|
||||
return ret
|
@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Edward Nunez <edward.nunez@cyberark.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: 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_params values please check parameters for clipasswordsdk in CyberArk's "Credential Provider and ASCP Implementation Guide"
|
||||
notes:
|
||||
- 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
|
||||
ansible.builtin.debug:
|
||||
msg: '{{ lookup("community.general.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
|
||||
ansible.builtin.debug:
|
||||
msg: "{{item}}"
|
||||
with_community.general.cyberarkpassword:
|
||||
appid: 'app_ansible'
|
||||
query: 'safe=CyberArk_Passwords;folder=root;object=AdminPass'
|
||||
output: 'Password,PassProps.UserName,PassProps.Address,PasswordChangeInProcess'
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_result:
|
||||
description: A list containing one dictionary.
|
||||
type: list
|
||||
elements: dictionary
|
||||
contains:
|
||||
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.module_utils.common.text.converters 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
|
@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015-2021, Felix Fontein <felix@fontein.de>
|
||||
# Copyright (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
name: dependent
|
||||
short_description: Composes a list with nested elements of other lists or dicts which can depend on previous loop variables
|
||||
author: Felix Fontein (@felixfontein)
|
||||
version_added: 3.1.0
|
||||
description:
|
||||
- "Takes the input lists and returns a list with elements that are lists, dictionaries,
|
||||
or template expressions which evaluate to lists or dicts, composed of the elements of
|
||||
the input evaluated lists and dictionaries."
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- A list where the elements are one-element dictionaries, mapping a name to a string, list, or dictionary.
|
||||
The name is the index that is used in the result object. The value is iterated over as described below.
|
||||
- If the value is a list, it is simply iterated over.
|
||||
- If the value is a dictionary, it is iterated over and returned as if they would be processed by the
|
||||
P(ansible.builtin.dict2items#filter) filter.
|
||||
- If the value is a string, it is evaluated as Jinja2 expressions which can access the previously chosen
|
||||
elements with C(item.<index_name>). The result must be a list or a dictionary.
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Install/remove public keys for active admin users
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ item.admin.key }}"
|
||||
key: "{{ lookup('file', item.key.public_key) }}"
|
||||
state: "{{ 'present' if item.key.active else 'absent' }}"
|
||||
when: item.admin.value.active
|
||||
with_community.general.dependent:
|
||||
- admin: admin_user_data
|
||||
- key: admin_ssh_keys[item.admin.key]
|
||||
loop_control:
|
||||
# Makes the output readable, so that it doesn't contain the whole subdictionaries and lists
|
||||
label: "{{ [item.admin.key, 'active' if item.key.active else 'inactive', item.key.public_key] }}"
|
||||
vars:
|
||||
admin_user_data:
|
||||
admin1:
|
||||
name: Alice
|
||||
active: true
|
||||
admin2:
|
||||
name: Bob
|
||||
active: true
|
||||
admin_ssh_keys:
|
||||
admin1:
|
||||
- private_key: keys/private_key_admin1.pem
|
||||
public_key: keys/private_key_admin1.pub
|
||||
active: true
|
||||
admin2:
|
||||
- private_key: keys/private_key_admin2.pem
|
||||
public_key: keys/private_key_admin2.pub
|
||||
active: true
|
||||
- private_key: keys/private_key_admin2-old.pem
|
||||
public_key: keys/private_key_admin2-old.pub
|
||||
active: false
|
||||
|
||||
- name: Update DNS records
|
||||
community.aws.route53:
|
||||
zone: "{{ item.zone.key }}"
|
||||
record: "{{ item.prefix.key ~ '.' if item.prefix.key else '' }}{{ item.zone.key }}"
|
||||
type: "{{ item.entry.key }}"
|
||||
ttl: "{{ item.entry.value.ttl | default(3600) }}"
|
||||
value: "{{ item.entry.value.value }}"
|
||||
state: "{{ 'absent' if (item.entry.value.absent | default(False)) else 'present' }}"
|
||||
overwrite: true
|
||||
loop_control:
|
||||
# Makes the output readable, so that it doesn't contain the whole subdictionaries and lists
|
||||
label: |-
|
||||
{{ [item.zone.key, item.prefix.key, item.entry.key,
|
||||
item.entry.value.ttl | default(3600),
|
||||
item.entry.value.absent | default(False), item.entry.value.value] }}
|
||||
with_community.general.dependent:
|
||||
- zone: dns_setup
|
||||
- prefix: item.zone.value
|
||||
- entry: item.prefix.value
|
||||
vars:
|
||||
dns_setup:
|
||||
example.com:
|
||||
'':
|
||||
A:
|
||||
value:
|
||||
- 1.2.3.4
|
||||
AAAA:
|
||||
value:
|
||||
- "2a01:1:2:3::1"
|
||||
'test._domainkey':
|
||||
TXT:
|
||||
ttl: 300
|
||||
value:
|
||||
- '"k=rsa; t=s; p=MIGfMA..."'
|
||||
example.org:
|
||||
'www':
|
||||
A:
|
||||
value:
|
||||
- 1.2.3.4
|
||||
- 5.6.7.8
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- A list composed of dictionaries whose keys are the variable names from the input list.
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
- key1: a
|
||||
key2: test
|
||||
- key1: a
|
||||
key2: foo
|
||||
- key1: b
|
||||
key2: bar
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils.common._collections_compat import Mapping, Sequence
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible.template import Templar
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
|
||||
# Whether Templar has a cache, which can be controlled by Templar.template()'s cache option.
|
||||
# The cache was removed for ansible-core 2.14 (https://github.com/ansible/ansible/pull/78419)
|
||||
_TEMPLAR_HAS_TEMPLATE_CACHE = LooseVersion(ansible_version) < LooseVersion('2.14.0')
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def __evaluate(self, expression, templar, variables):
|
||||
"""Evaluate expression with templar.
|
||||
|
||||
``expression`` is the expression to evaluate.
|
||||
``variables`` are the variables to use.
|
||||
"""
|
||||
templar.available_variables = variables or {}
|
||||
expression = "{0}{1}{2}".format("{{", expression, "}}")
|
||||
if _TEMPLAR_HAS_TEMPLATE_CACHE:
|
||||
return templar.template(expression, cache=False)
|
||||
return templar.template(expression)
|
||||
|
||||
def __process(self, result, terms, index, current, templar, variables):
|
||||
"""Fills ``result`` list with evaluated items.
|
||||
|
||||
``result`` is a list where the resulting items are placed.
|
||||
``terms`` is the parsed list of terms
|
||||
``index`` is the current index to be processed in the list.
|
||||
``current`` is a dictionary where the first ``index`` values are filled in.
|
||||
``variables`` are the variables currently available.
|
||||
"""
|
||||
# If we are done, add to result list:
|
||||
if index == len(terms):
|
||||
result.append(current.copy())
|
||||
return
|
||||
|
||||
key, expression, values = terms[index]
|
||||
|
||||
if expression is not None:
|
||||
# Evaluate expression in current context
|
||||
vars = variables.copy()
|
||||
vars['item'] = current.copy()
|
||||
try:
|
||||
values = self.__evaluate(expression, templar, variables=vars)
|
||||
except Exception as e:
|
||||
raise AnsibleLookupError(
|
||||
'Caught "{error}" while evaluating {key!r} with item == {item!r}'.format(
|
||||
error=e, key=key, item=current))
|
||||
|
||||
if isinstance(values, Mapping):
|
||||
for idx, val in sorted(values.items()):
|
||||
current[key] = dict([('key', idx), ('value', val)])
|
||||
self.__process(result, terms, index + 1, current, templar, variables)
|
||||
elif isinstance(values, Sequence):
|
||||
for elt in values:
|
||||
current[key] = elt
|
||||
self.__process(result, terms, index + 1, current, templar, variables)
|
||||
else:
|
||||
raise AnsibleLookupError(
|
||||
'Did not obtain dictionary or list while evaluating {key!r} with item == {item!r}, but {type}'.format(
|
||||
key=key, item=current, type=type(values)))
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
"""Generate list."""
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
result = []
|
||||
if len(terms) > 0:
|
||||
templar = Templar(loader=self._templar._loader)
|
||||
data = []
|
||||
vars_so_far = set()
|
||||
for index, term in enumerate(terms):
|
||||
if not isinstance(term, Mapping):
|
||||
raise AnsibleLookupError(
|
||||
'Parameter {index} must be a dictionary, got {type}'.format(
|
||||
index=index, type=type(term)))
|
||||
if len(term) != 1:
|
||||
raise AnsibleLookupError(
|
||||
'Parameter {index} must be a one-element dictionary, got {count} elements'.format(
|
||||
index=index, count=len(term)))
|
||||
k, v = list(term.items())[0]
|
||||
if k in vars_so_far:
|
||||
raise AnsibleLookupError(
|
||||
'The variable {key!r} appears more than once'.format(key=k))
|
||||
vars_so_far.add(k)
|
||||
if isinstance(v, string_types):
|
||||
data.append((k, v, None))
|
||||
elif isinstance(v, (Sequence, Mapping)):
|
||||
data.append((k, None, v))
|
||||
else:
|
||||
raise AnsibleLookupError(
|
||||
'Parameter {key!r} (index {index}) must have a value of type string, dictionary or list, got type {type}'.format(
|
||||
index=index, key=k, type=type(v)))
|
||||
self.__process(result, data, 0, {}, templar, variables)
|
||||
return result
|
@ -0,0 +1,459 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: 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 C(wantlist=true) to the lookup call, or alternatively use C(query) instead of C(lookup),
|
||||
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.
|
||||
type: list
|
||||
elements: str
|
||||
qtype:
|
||||
description:
|
||||
- Record type to query.
|
||||
- V(DLV) has been removed in community.general 6.0.0.
|
||||
- V(CAA) has been added in community.general 6.3.0.
|
||||
type: str
|
||||
default: 'A'
|
||||
choices: [A, ALL, AAAA, CAA, CNAME, DNAME, 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.
|
||||
type: int
|
||||
default: 1
|
||||
retry_servfail:
|
||||
description: Retry a nameserver if it returns SERVFAIL.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 3.6.0
|
||||
fail_on_error:
|
||||
description:
|
||||
- Abort execution on lookup errors.
|
||||
- The default for this option will likely change to V(true) in the future.
|
||||
The current default, V(false), is used for backwards compatibility, and will result in empty strings
|
||||
or the string V(NXDOMAIN) in the result in case of errors.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 5.4.0
|
||||
real_empty:
|
||||
description:
|
||||
- Return empty result without empty strings, and return empty list instead of V(NXDOMAIN).
|
||||
- The default for this option will likely change to V(true) in the future.
|
||||
- This option will be forced to V(true) if multiple domains to be queried are specified.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 6.0.0
|
||||
class:
|
||||
description:
|
||||
- "Class."
|
||||
type: str
|
||||
default: 'IN'
|
||||
tcp:
|
||||
description: Use TCP to lookup DNS records.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 7.5.0
|
||||
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
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.dig', 'example.com.')}}"
|
||||
|
||||
- name: "The TXT record for example.org."
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.dig', 'example.org.', qtype='TXT') }}"
|
||||
|
||||
- name: "The TXT record for example.org, alternative syntax."
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.dig', 'example.org./TXT') }}"
|
||||
|
||||
- name: use in a loop
|
||||
ansible.builtin.debug:
|
||||
msg: "MX record for gmail.com {{ item }}"
|
||||
with_items: "{{ lookup('community.general.dig', 'gmail.com./MX', wantlist=true) }}"
|
||||
|
||||
- name: Lookup multiple names at once
|
||||
ansible.builtin.debug:
|
||||
msg: "A record found {{ item }}"
|
||||
loop: "{{ query('community.general.dig', 'example.org.', 'example.com.', 'gmail.com.') }}"
|
||||
|
||||
- name: Lookup multiple names at once (from list variable)
|
||||
ansible.builtin.debug:
|
||||
msg: "A record found {{ item }}"
|
||||
loop: "{{ query('community.general.dig', *hosts) }}"
|
||||
vars:
|
||||
hosts:
|
||||
- example.org.
|
||||
- example.com.
|
||||
- gmail.com.
|
||||
|
||||
- ansible.builtin.debug:
|
||||
msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '192.0.2.5/PTR') }}"
|
||||
- ansible.builtin.debug:
|
||||
msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '5.2.0.192.in-addr.arpa./PTR') }}"
|
||||
- ansible.builtin.debug:
|
||||
msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '5.2.0.192.in-addr.arpa.', qtype='PTR') }}"
|
||||
- ansible.builtin.debug:
|
||||
msg: "Querying 198.51.100.23 for IPv4 address for example.com. produces {{ lookup('dig', 'example.com', '@198.51.100.23') }}"
|
||||
|
||||
- ansible.builtin.debug:
|
||||
msg: "XMPP service for gmail.com. is available at {{ item.target }} on port {{ item.port }}"
|
||||
with_items: "{{ lookup('community.general.dig', '_xmpp-server._tcp.gmail.com./SRV', flat=0, wantlist=true) }}"
|
||||
|
||||
- name: Retry nameservers that return SERVFAIL
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.dig', 'example.org./A', retry_servfail=true) }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- List of composed strings or dictionaries with key and value
|
||||
If a dictionary, fields shows the keys returned depending on query type
|
||||
type: list
|
||||
elements: raw
|
||||
contains:
|
||||
ALL:
|
||||
description:
|
||||
- owner, ttl, type
|
||||
A:
|
||||
description:
|
||||
- address
|
||||
AAAA:
|
||||
description:
|
||||
- address
|
||||
CAA:
|
||||
description:
|
||||
- flags
|
||||
- tag
|
||||
- value
|
||||
version_added: 6.3.0
|
||||
CNAME:
|
||||
description:
|
||||
- target
|
||||
DNAME:
|
||||
description:
|
||||
- target
|
||||
DNSKEY:
|
||||
description:
|
||||
- flags, algorithm, protocol, key
|
||||
DS:
|
||||
description:
|
||||
- algorithm, digest_type, key_tag, digest
|
||||
HINFO:
|
||||
description:
|
||||
- cpu, os
|
||||
LOC:
|
||||
description:
|
||||
- latitude, longitude, altitude, size, horizontal_precision, vertical_precision
|
||||
MX:
|
||||
description:
|
||||
- preference, exchange
|
||||
NAPTR:
|
||||
description:
|
||||
- order, preference, flags, service, regexp, replacement
|
||||
NS:
|
||||
description:
|
||||
- target
|
||||
NSEC3PARAM:
|
||||
description:
|
||||
- algorithm, flags, iterations, salt
|
||||
PTR:
|
||||
description:
|
||||
- target
|
||||
RP:
|
||||
description:
|
||||
- mbox, txt
|
||||
SOA:
|
||||
description:
|
||||
- mname, rname, serial, refresh, retry, expire, minimum
|
||||
SPF:
|
||||
description:
|
||||
- strings
|
||||
SRV:
|
||||
description:
|
||||
- priority, weight, port, target
|
||||
SSHFP:
|
||||
description:
|
||||
- algorithm, fp_type, fingerprint
|
||||
TLSA:
|
||||
description:
|
||||
- usage, selector, mtype, cert
|
||||
TXT:
|
||||
description:
|
||||
- strings
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.utils.display import Display
|
||||
import socket
|
||||
|
||||
try:
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.resolver
|
||||
import dns.reversename
|
||||
import dns.rdataclass
|
||||
from dns.rdatatype import (A, AAAA, CAA, CNAME, 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
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
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'],
|
||||
CAA: ['flags', 'tag', 'value'],
|
||||
CNAME: ['target'],
|
||||
DNAME: ['target'],
|
||||
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: ['type_covered', 'algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'key_tag', 'signer', '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 == DS and f == 'digest':
|
||||
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
||||
if rdata.rdtype == DNSKEY and f == 'algorithm':
|
||||
val = int(val)
|
||||
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")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
# 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)
|
||||
|
||||
domains = []
|
||||
qtype = self.get_option('qtype')
|
||||
flat = self.get_option('flat')
|
||||
fail_on_error = self.get_option('fail_on_error')
|
||||
real_empty = self.get_option('real_empty')
|
||||
tcp = self.get_option('tcp')
|
||||
try:
|
||||
rdclass = dns.rdataclass.from_text(self.get_option('class'))
|
||||
except Exception as e:
|
||||
raise AnsibleError("dns lookup illegal CLASS: %s" % to_native(e))
|
||||
myres.retry_servfail = self.get_option('retry_servfail')
|
||||
|
||||
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('=', 1)
|
||||
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))
|
||||
elif opt == 'retry_servfail':
|
||||
myres.retry_servfail = boolean(arg)
|
||||
elif opt == 'fail_on_error':
|
||||
fail_on_error = boolean(arg)
|
||||
elif opt == 'real_empty':
|
||||
real_empty = boolean(arg)
|
||||
elif opt == 'tcp':
|
||||
tcp = boolean(arg)
|
||||
|
||||
continue
|
||||
|
||||
if '/' in t:
|
||||
try:
|
||||
domain, qtype = t.split('/')
|
||||
domains.append(domain)
|
||||
except Exception:
|
||||
domains.append(t)
|
||||
else:
|
||||
domains.append(t)
|
||||
|
||||
# print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
|
||||
|
||||
if qtype.upper() == 'PTR':
|
||||
reversed_domains = []
|
||||
for domain in domains:
|
||||
try:
|
||||
n = dns.reversename.from_address(domain)
|
||||
reversed_domains.append(n.to_text())
|
||||
except dns.exception.SyntaxError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
|
||||
domains = reversed_domains
|
||||
|
||||
if len(domains) > 1:
|
||||
real_empty = True
|
||||
|
||||
ret = []
|
||||
|
||||
for domain in domains:
|
||||
try:
|
||||
answers = myres.query(domain, qtype, rdclass=rdclass, tcp=tcp)
|
||||
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 err:
|
||||
if fail_on_error:
|
||||
raise AnsibleError("Lookup failed: %s" % str(err))
|
||||
ret.append(str(err))
|
||||
|
||||
except dns.resolver.NXDOMAIN as err:
|
||||
if fail_on_error:
|
||||
raise AnsibleError("Lookup failed: %s" % str(err))
|
||||
if not real_empty:
|
||||
ret.append('NXDOMAIN')
|
||||
except dns.resolver.NoAnswer as err:
|
||||
if fail_on_error:
|
||||
raise AnsibleError("Lookup failed: %s" % str(err))
|
||||
if not real_empty:
|
||||
ret.append("")
|
||||
except dns.resolver.Timeout as err:
|
||||
if fail_on_error:
|
||||
raise AnsibleError("Lookup failed: %s" % str(err))
|
||||
if not real_empty:
|
||||
ret.append("")
|
||||
except dns.exception.DNSException as err:
|
||||
raise AnsibleError("dns.resolver unhandled exception %s" % to_native(err))
|
||||
|
||||
return ret
|
@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2012, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: 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
|
||||
elements: string
|
||||
real_empty:
|
||||
description:
|
||||
- Return empty result without empty strings, and return empty list instead of V(NXDOMAIN).
|
||||
- The default for this option will likely change to V(true) in the future.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 6.0.0
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: show txt entry
|
||||
ansible.builtin.debug:
|
||||
msg: "{{lookup('community.general.dnstxt', ['test.example.com'])}}"
|
||||
|
||||
- name: iterate over txt entries
|
||||
ansible.builtin.debug:
|
||||
msg: "{{item}}"
|
||||
with_community.general.dnstxt:
|
||||
- 'test.example.com'
|
||||
- 'other.example.com'
|
||||
- 'last.example.com'
|
||||
|
||||
- name: iterate of a comma delimited DNS TXT entry
|
||||
ansible.builtin.debug:
|
||||
msg: "{{item}}"
|
||||
with_community.general.dnstxt: "{{lookup('community.general.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.common.text.converters 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):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
if HAVE_DNS is False:
|
||||
raise AnsibleError("Can't LOOKUP(dnstxt): module dns.resolver is not installed")
|
||||
|
||||
real_empty = self.get_option('real_empty')
|
||||
|
||||
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:
|
||||
if real_empty:
|
||||
continue
|
||||
string = 'NXDOMAIN'
|
||||
except dns.resolver.Timeout:
|
||||
if real_empty:
|
||||
continue
|
||||
string = ''
|
||||
except dns.resolver.NoAnswer:
|
||||
if real_empty:
|
||||
continue
|
||||
string = ''
|
||||
except DNSException as e:
|
||||
raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e))
|
||||
|
||||
ret.append(''.join(string))
|
||||
|
||||
return ret
|
@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Adam Migus <adam@migus.org>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: dsv
|
||||
author: Adam Migus (@amigus) <adam@migus.org>
|
||||
short_description: Get secrets from Thycotic DevOps Secrets Vault
|
||||
version_added: 1.0.0
|
||||
description:
|
||||
- Uses the Thycotic DevOps Secrets Vault Python SDK to get Secrets from a
|
||||
DSV O(tenant) using a O(client_id) and O(client_secret).
|
||||
requirements:
|
||||
- python-dsv-sdk - https://pypi.org/project/python-dsv-sdk/
|
||||
options:
|
||||
_terms:
|
||||
description: The path to the secret, for example V(/staging/servers/web1).
|
||||
required: true
|
||||
tenant:
|
||||
description: The first format parameter in the default O(url_template).
|
||||
env:
|
||||
- name: DSV_TENANT
|
||||
ini:
|
||||
- section: dsv_lookup
|
||||
key: tenant
|
||||
required: true
|
||||
tld:
|
||||
default: com
|
||||
description: The top-level domain of the tenant; the second format
|
||||
parameter in the default O(url_template).
|
||||
env:
|
||||
- name: DSV_TLD
|
||||
ini:
|
||||
- section: dsv_lookup
|
||||
key: tld
|
||||
required: false
|
||||
client_id:
|
||||
description: The client_id with which to request the Access Grant.
|
||||
env:
|
||||
- name: DSV_CLIENT_ID
|
||||
ini:
|
||||
- section: dsv_lookup
|
||||
key: client_id
|
||||
required: true
|
||||
client_secret:
|
||||
description: The client secret associated with the specific O(client_id).
|
||||
env:
|
||||
- name: DSV_CLIENT_SECRET
|
||||
ini:
|
||||
- section: dsv_lookup
|
||||
key: client_secret
|
||||
required: true
|
||||
url_template:
|
||||
default: https://{}.secretsvaultcloud.{}/v1
|
||||
description: The path to prepend to the base URL to form a valid REST
|
||||
API request.
|
||||
env:
|
||||
- name: DSV_URL_TEMPLATE
|
||||
ini:
|
||||
- section: dsv_lookup
|
||||
key: url_template
|
||||
required: false
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_list:
|
||||
description:
|
||||
- One or more JSON responses to C(GET /secrets/{path}).
|
||||
- See U(https://dsv.thycotic.com/api/index.html#operation/getSecret).
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: "{{ lookup('community.general.dsv', '/test/secret') }}"
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: 'the password is {{ secret["data"]["password"] }}'
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
|
||||
sdk_is_missing = False
|
||||
|
||||
try:
|
||||
from thycotic.secrets.vault import (
|
||||
SecretsVault,
|
||||
SecretsVaultError,
|
||||
)
|
||||
except ImportError:
|
||||
sdk_is_missing = True
|
||||
|
||||
from ansible.utils.display import Display
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
@staticmethod
|
||||
def Client(vault_parameters):
|
||||
try:
|
||||
vault = SecretsVault(**vault_parameters)
|
||||
return vault
|
||||
except TypeError:
|
||||
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
if sdk_is_missing:
|
||||
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
vault = LookupModule.Client(
|
||||
{
|
||||
"tenant": self.get_option("tenant"),
|
||||
"client_id": self.get_option("client_id"),
|
||||
"client_secret": self.get_option("client_secret"),
|
||||
"tld": self.get_option("tld"),
|
||||
"url_template": self.get_option("url_template"),
|
||||
}
|
||||
)
|
||||
result = []
|
||||
|
||||
for term in terms:
|
||||
display.debug("dsv_lookup term: %s" % term)
|
||||
try:
|
||||
path = term.lstrip("[/:]")
|
||||
|
||||
if path == "":
|
||||
raise AnsibleOptionsError("Invalid secret path: %s" % term)
|
||||
|
||||
display.vvv(u"DevOps Secrets Vault GET /secrets/%s" % path)
|
||||
result.append(vault.get_secret_json(path))
|
||||
except SecretsVaultError as error:
|
||||
raise AnsibleError(
|
||||
"DevOps Secrets Vault lookup failure: %s" % error.message
|
||||
)
|
||||
return result
|
@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# (m) 2016, Mihai Moldovanu <mihaim@tfm.ro>
|
||||
# (m) 2017, Juan Manuel Parrilla <jparrill@redhat.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens)
|
||||
name: 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
|
||||
seealso:
|
||||
- module: community.general.etcd3
|
||||
- plugin: community.general.etcd3
|
||||
plugin_type: lookup
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "a value from a locally running etcd"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd', 'foo/bar') }}"
|
||||
|
||||
- name: "values from multiple folders on a locally running etcd"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd', 'foo', 'bar', 'baz') }}"
|
||||
|
||||
- name: "you can set server options inline"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd', 'foo', version='v2', url='http://192.168.0.27:4001') }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description:
|
||||
- List of values associated with input keys.
|
||||
type: list
|
||||
elements: string
|
||||
'''
|
||||
|
||||
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
|
||||
#
|
||||
# - ansible.builtin.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
|
@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2020, SCC France, Eric Belhomme <ebelhomme@fr.scc.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Eric Belhomme (@eric-belhomme) <ebelhomme@fr.scc.com>
|
||||
version_added: '0.2.0'
|
||||
name: etcd3
|
||||
short_description: Get key values from etcd3 server
|
||||
description:
|
||||
- Retrieves key values and/or key prefixes from etcd3 server using its native gRPC API.
|
||||
- Try to reuse M(community.general.etcd3) options for connection parameters, but add support for some C(ETCDCTL_*) environment variables.
|
||||
- See U(https://github.com/etcd-io/etcd/tree/master/Documentation/op-guide) for etcd overview.
|
||||
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- The list of keys (or key prefixes) to look up on the etcd3 server.
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
prefix:
|
||||
description:
|
||||
- Look for key or prefix key.
|
||||
type: bool
|
||||
default: false
|
||||
endpoints:
|
||||
description:
|
||||
- Counterpart of E(ETCDCTL_ENDPOINTS) environment variable.
|
||||
Specify the etcd3 connection with and URL form, for example V(https://hostname:2379), or V(<host>:<port>) form.
|
||||
- The V(host) part is overwritten by O(host) option, if defined.
|
||||
- The V(port) part is overwritten by O(port) option, if defined.
|
||||
env:
|
||||
- name: ETCDCTL_ENDPOINTS
|
||||
default: '127.0.0.1:2379'
|
||||
type: str
|
||||
host:
|
||||
description:
|
||||
- etcd3 listening client host.
|
||||
- Takes precedence over O(endpoints).
|
||||
type: str
|
||||
port:
|
||||
description:
|
||||
- etcd3 listening client port.
|
||||
- Takes precedence over O(endpoints).
|
||||
type: int
|
||||
ca_cert:
|
||||
description:
|
||||
- etcd3 CA authority.
|
||||
env:
|
||||
- name: ETCDCTL_CACERT
|
||||
type: str
|
||||
cert_cert:
|
||||
description:
|
||||
- etcd3 client certificate.
|
||||
env:
|
||||
- name: ETCDCTL_CERT
|
||||
type: str
|
||||
cert_key:
|
||||
description:
|
||||
- etcd3 client private key.
|
||||
env:
|
||||
- name: ETCDCTL_KEY
|
||||
type: str
|
||||
timeout:
|
||||
description:
|
||||
- Client timeout.
|
||||
default: 60
|
||||
env:
|
||||
- name: ETCDCTL_DIAL_TIMEOUT
|
||||
type: int
|
||||
user:
|
||||
description:
|
||||
- Authenticated user name.
|
||||
env:
|
||||
- name: ETCDCTL_USER
|
||||
type: str
|
||||
password:
|
||||
description:
|
||||
- Authenticated user password.
|
||||
env:
|
||||
- name: ETCDCTL_PASSWORD
|
||||
type: str
|
||||
|
||||
notes:
|
||||
- O(host) and O(port) options take precedence over (endpoints) option.
|
||||
- The recommended way to connect to etcd3 server is using E(ETCDCTL_ENDPOINT)
|
||||
environment variable and keep O(endpoints), O(host), and O(port) unused.
|
||||
seealso:
|
||||
- module: community.general.etcd3
|
||||
- plugin: community.general.etcd
|
||||
plugin_type: lookup
|
||||
|
||||
requirements:
|
||||
- "etcd3 >= 0.10"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "a value from a locally running etcd"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd3', 'foo/bar') }}"
|
||||
|
||||
- name: "values from multiple folders on a locally running etcd"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd3', 'foo', 'bar', 'baz') }}"
|
||||
|
||||
- name: "look for a key prefix"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd3', '/foo/bar', prefix=True) }}"
|
||||
|
||||
- name: "connect to etcd3 with a client certificate"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.etcd3', 'foo/bar', cert_cert='/etc/ssl/etcd/client.pem', cert_key='/etc/ssl/etcd/client.key') }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description:
|
||||
- List of keys and associated values.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
key:
|
||||
description: The element's key.
|
||||
type: str
|
||||
value:
|
||||
description: The element's value.
|
||||
type: str
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
try:
|
||||
import etcd3
|
||||
HAS_ETCD = True
|
||||
except ImportError:
|
||||
HAS_ETCD = False
|
||||
|
||||
display = Display()
|
||||
|
||||
etcd3_cnx_opts = (
|
||||
'host',
|
||||
'port',
|
||||
'ca_cert',
|
||||
'cert_key',
|
||||
'cert_cert',
|
||||
'timeout',
|
||||
'user',
|
||||
'password',
|
||||
# 'grpc_options' Etcd3Client() option currently not supported by lookup module (maybe in future ?)
|
||||
)
|
||||
|
||||
|
||||
def etcd3_client(client_params):
|
||||
try:
|
||||
etcd = etcd3.client(**client_params)
|
||||
etcd.status()
|
||||
except Exception as exp:
|
||||
raise AnsibleLookupError('Cannot connect to etcd cluster: %s' % (to_native(exp)))
|
||||
return etcd
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
if not HAS_ETCD:
|
||||
display.error(missing_required_lib('etcd3'))
|
||||
return None
|
||||
|
||||
# create the etcd3 connection parameters dict to pass to etcd3 class
|
||||
client_params = {}
|
||||
|
||||
# etcd3 class expects host and port as connection parameters, so endpoints
|
||||
# must be mangled a bit to fit in this scheme.
|
||||
# so here we use a regex to extract server and port
|
||||
match = re.compile(
|
||||
r'^(https?://)?(?P<host>(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([-_\d\w\.]+))(:(?P<port>\d{1,5}))?/?$'
|
||||
).match(self.get_option('endpoints'))
|
||||
if match:
|
||||
if match.group('host'):
|
||||
client_params['host'] = match.group('host')
|
||||
if match.group('port'):
|
||||
client_params['port'] = match.group('port')
|
||||
|
||||
for opt in etcd3_cnx_opts:
|
||||
if self.get_option(opt):
|
||||
client_params[opt] = self.get_option(opt)
|
||||
|
||||
cnx_log = dict(client_params)
|
||||
if 'password' in cnx_log:
|
||||
cnx_log['password'] = '<redacted>'
|
||||
display.verbose("etcd3 connection parameters: %s" % cnx_log)
|
||||
|
||||
# connect to etcd3 server
|
||||
etcd = etcd3_client(client_params)
|
||||
|
||||
ret = []
|
||||
# we can pass many keys to lookup
|
||||
for term in terms:
|
||||
if self.get_option('prefix'):
|
||||
try:
|
||||
for val, meta in etcd.get_prefix(term):
|
||||
if val and meta:
|
||||
ret.append({'key': to_native(meta.key), 'value': to_native(val)})
|
||||
except Exception as exp:
|
||||
display.warning('Caught except during etcd3.get_prefix: %s' % (to_native(exp)))
|
||||
else:
|
||||
try:
|
||||
val, meta = etcd.get(term)
|
||||
if val and meta:
|
||||
ret.append({'key': to_native(meta.key), 'value': to_native(val)})
|
||||
except Exception as exp:
|
||||
display.warning('Caught except during etcd3.get: %s' % (to_native(exp)))
|
||||
return ret
|
@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Dag Wieers <dag@wieers.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
name: 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 = r"""
|
||||
- name: Create directories
|
||||
ansible.builtin.file:
|
||||
path: /web/{{ item.path }}
|
||||
state: directory
|
||||
mode: '{{ item.mode }}'
|
||||
with_community.general.filetree: web/
|
||||
when: item.state == 'directory'
|
||||
|
||||
- name: Template files (explicitly skip directories in order to use the 'src' attribute)
|
||||
ansible.builtin.template:
|
||||
src: '{{ item.src }}'
|
||||
# Your template files should be stored with a .j2 file extension,
|
||||
# but should not be deployed with it. splitext|first removes it.
|
||||
dest: /web/{{ item.path | splitext | first }}
|
||||
mode: '{{ item.mode }}'
|
||||
with_community.general.filetree: web/
|
||||
when: item.state == 'file'
|
||||
|
||||
- name: Recreate symlinks
|
||||
ansible.builtin.file:
|
||||
src: '{{ item.src }}'
|
||||
dest: /web/{{ item.path }}
|
||||
state: link
|
||||
follow: false # avoid corrupting target files if the link already exists
|
||||
force: true
|
||||
mode: '{{ item.mode }}'
|
||||
with_community.general.filetree: web/
|
||||
when: item.state == 'link'
|
||||
|
||||
- name: list all files under web/
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.filetree', 'web/') }}"
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_raw:
|
||||
description: List of dictionaries with file information.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
src:
|
||||
description:
|
||||
- Full path to file.
|
||||
- Not returned when RV(_raw[].state) is set to V(directory).
|
||||
type: path
|
||||
root:
|
||||
description: Allows filtering by original location.
|
||||
type: path
|
||||
path:
|
||||
description: Contains the relative path to root.
|
||||
type: path
|
||||
mode:
|
||||
description: The permissions the resulting file or directory.
|
||||
type: str
|
||||
state:
|
||||
description: TODO
|
||||
type: str
|
||||
owner:
|
||||
description: Name of the user that owns the file/directory.
|
||||
type: raw
|
||||
group:
|
||||
description: Name of the group that owns the file/directory.
|
||||
type: raw
|
||||
seuser:
|
||||
description: The user part of the SELinux file context.
|
||||
type: raw
|
||||
serole:
|
||||
description: The role part of the SELinux file context.
|
||||
type: raw
|
||||
setype:
|
||||
description: The type part of the SELinux file context.
|
||||
type: raw
|
||||
selevel:
|
||||
description: The level part of the SELinux file context.
|
||||
type: raw
|
||||
uid:
|
||||
description: Owner ID of the file/directory.
|
||||
type: int
|
||||
gid:
|
||||
description: Group ID of the file/directory.
|
||||
type: int
|
||||
size:
|
||||
description: Size of the target.
|
||||
type: int
|
||||
mtime:
|
||||
description: Time of last modification.
|
||||
type: float
|
||||
ctime:
|
||||
description: Time of last metadata update or creation (depends on OS).
|
||||
type: float
|
||||
"""
|
||||
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.common.text.converters 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):
|
||||
self.set_options(var_options=variables, direct=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
|
@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: flattened
|
||||
author: Serge van Ginderachter (!UNKNOWN) <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
|
||||
type: list
|
||||
elements: raw
|
||||
required: true
|
||||
notes:
|
||||
- Unlike the P(ansible.builtin.items#lookup) lookup 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"
|
||||
ansible.builtin.debug:
|
||||
msg: "all in one list {{lookup('community.general.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
|
||||
try:
|
||||
term2 = listify_lookup_plugin_terms(term, templar=self._templar)
|
||||
except TypeError:
|
||||
# The loader argument is deprecated in ansible-core 2.14+. Fall back to
|
||||
# pre-2.14 behavior for older ansible-core versions.
|
||||
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=None, **kwargs):
|
||||
if not isinstance(terms, list):
|
||||
raise AnsibleError("with_flattened expects a list")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
return self._do_flatten(terms, variables)
|
@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023, Poh Wei Sheng <weisheng-p@hotmail.sg>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: github_app_access_token
|
||||
author:
|
||||
- Poh Wei Sheng (@weisheng-p)
|
||||
short_description: Obtain short-lived Github App Access tokens
|
||||
version_added: '8.2.0'
|
||||
requirements:
|
||||
- jwt (https://github.com/GehirnInc/python-jwt)
|
||||
description:
|
||||
- This generates a Github access token that can be used with a C(git) command, if you use a Github App.
|
||||
options:
|
||||
key_path:
|
||||
description:
|
||||
- Path to your private key.
|
||||
required: true
|
||||
type: path
|
||||
app_id:
|
||||
description:
|
||||
- Your GitHub App ID, you can find this in the Settings page.
|
||||
required: true
|
||||
type: str
|
||||
installation_id:
|
||||
description:
|
||||
- The installation ID that contains the git repository you would like access to.
|
||||
- As of 2023-12-24, this can be found via Settings page > Integrations > Application. The last part of the URL in the
|
||||
configure button is the installation ID.
|
||||
- Alternatively, you can use PyGithub (U(https://github.com/PyGithub/PyGithub)) to get your installation ID.
|
||||
required: true
|
||||
type: str
|
||||
token_expiry:
|
||||
description:
|
||||
- How long the token should last for in seconds.
|
||||
default: 600
|
||||
type: int
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Get access token to be used for git checkout with app_id=123456, installation_id=64209
|
||||
ansible.builtin.git:
|
||||
repo: >-
|
||||
https://x-access-token:{{ github_token }}@github.com/hidden_user/super-secret-repo.git
|
||||
dest: /srv/checkout
|
||||
vars:
|
||||
github_token: >-
|
||||
lookup('community.general.github_app_access_token', key_path='/home/to_your/key',
|
||||
app_id='123456', installation_id='64209')
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
_raw:
|
||||
description: A one-element list containing your GitHub access token.
|
||||
type: list
|
||||
elements: str
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
from jwt import JWT, jwk_from_pem
|
||||
HAS_JWT = True
|
||||
except ImportError:
|
||||
HAS_JWT = False
|
||||
|
||||
import time
|
||||
import json
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
if HAS_JWT:
|
||||
jwt_instance = JWT()
|
||||
else:
|
||||
jwk_from_pem = None
|
||||
jwt_instance = None
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
def read_key(path):
|
||||
try:
|
||||
with open(path, 'rb') as pem_file:
|
||||
return jwk_from_pem(pem_file.read())
|
||||
except Exception as e:
|
||||
raise AnsibleError("Error while parsing key file: {0}".format(e))
|
||||
|
||||
|
||||
def encode_jwt(app_id, jwk, exp=600):
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'iat': now,
|
||||
'exp': now + exp,
|
||||
'iss': app_id,
|
||||
}
|
||||
try:
|
||||
return jwt_instance.encode(payload, jwk, alg='RS256')
|
||||
except Exception as e:
|
||||
raise AnsibleError("Error while encoding jwt: {0}".format(e))
|
||||
|
||||
|
||||
def post_request(generated_jwt, installation_id):
|
||||
github_api_url = f'https://api.github.com/app/installations/{installation_id}/access_tokens'
|
||||
headers = {
|
||||
"Authorization": f'Bearer {generated_jwt}',
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
try:
|
||||
response = open_url(github_api_url, headers=headers, method='POST')
|
||||
except HTTPError as e:
|
||||
try:
|
||||
error_body = json.loads(e.read().decode())
|
||||
display.vvv("Error returned: {0}".format(error_body))
|
||||
except Exception:
|
||||
error_body = {}
|
||||
if e.code == 404:
|
||||
raise AnsibleError("Github return error. Please confirm your installationd_id value is valid")
|
||||
elif e.code == 401:
|
||||
raise AnsibleError("Github return error. Please confirm your private key is valid")
|
||||
raise AnsibleError("Unexpected data returned: {0} -- {1}".format(e, error_body))
|
||||
response_body = response.read()
|
||||
try:
|
||||
json_data = json.loads(response_body.decode('utf-8'))
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise AnsibleError("Error while dencoding JSON respone from github: {0}".format(e))
|
||||
return json_data.get('token')
|
||||
|
||||
|
||||
def get_token(key_path, app_id, installation_id, expiry=600):
|
||||
jwk = read_key(key_path)
|
||||
generated_jwt = encode_jwt(app_id, jwk, exp=expiry)
|
||||
return post_request(generated_jwt, installation_id)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
if not HAS_JWT:
|
||||
raise AnsibleError('Python jwt library is required. '
|
||||
'Please install using "pip install jwt"')
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
t = get_token(
|
||||
self.get_option('key_path'),
|
||||
self.get_option('app_id'),
|
||||
self.get_option('installation_id'),
|
||||
self.get_option('token_expiry'),
|
||||
)
|
||||
|
||||
return [t]
|
@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Juan Manuel Parrilla <jparrill@redhat.com>
|
||||
# Copyright (c) 2012-17 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Juan Manuel Parrilla (@jparrill)
|
||||
name: 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:
|
||||
_terms:
|
||||
description:
|
||||
- The list of keys to lookup on the Puppetmaster.
|
||||
type: list
|
||||
elements: string
|
||||
required: true
|
||||
executable:
|
||||
description:
|
||||
- Binary file to execute Hiera.
|
||||
default: '/usr/bin/hiera'
|
||||
env:
|
||||
- name: ANSIBLE_HIERA_BIN
|
||||
config_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'"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.hiera', 'foo') }}"
|
||||
|
||||
- name: "a value from a Hiera 'DB' on other environment"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.hiera', 'foo environment=production') }}"
|
||||
|
||||
- name: "a value from a Hiera 'DB' for a concrete node"
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.hiera', 'foo fqdn=puppet01.localdomain') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- a value associated with input key
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.cmd_functions import run_cmd
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
|
||||
class Hiera(object):
|
||||
def __init__(self, hiera_cfg, hiera_bin):
|
||||
self.hiera_cfg = hiera_cfg
|
||||
self.hiera_bin = hiera_bin
|
||||
|
||||
def get(self, hiera_key):
|
||||
pargs = [self.hiera_bin]
|
||||
pargs.extend(['-c', self.hiera_cfg])
|
||||
|
||||
pargs.extend(hiera_key)
|
||||
|
||||
rc, output, err = run_cmd("{0} -c {1} {2}".format(
|
||||
self.hiera_bin, self.hiera_cfg, hiera_key[0]))
|
||||
|
||||
return to_text(output.strip())
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
hiera = Hiera(self.get_option('config_file'), self.get_option('executable'))
|
||||
ret = [hiera.get(terms)]
|
||||
return ret
|
@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016, Samuel Boucher <boucher.samuel.c@gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: keyring
|
||||
author:
|
||||
- Samuel Boucher (!UNKNOWN) <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)
|
||||
ansible.builtin.debug:
|
||||
msg: "Password: {{item}}"
|
||||
with_community.general.keyring:
|
||||
- 'servicename username'
|
||||
|
||||
- name: access mysql with password from keyring
|
||||
community.mysql.mysql_db:
|
||||
login_password: "{{ lookup('community.general.keyring', 'mysql joe') }}"
|
||||
login_user: joe
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: Secrets stored.
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
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, variables=None, **kwargs):
|
||||
if not HAS_KEYRING:
|
||||
raise AnsibleError(u"Can't LOOKUP(keyring): missing required python library 'keyring'")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
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
|
@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016, Andrew Zenk <azenk@umn.edu>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: lastpass
|
||||
author:
|
||||
- Andrew Zenk (!UNKNOWN) <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
|
||||
type: list
|
||||
elements: str
|
||||
field:
|
||||
description: Field to return from LastPass.
|
||||
default: 'password'
|
||||
type: str
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: get 'custom_field' from LastPass entry 'entry-name'
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.lastpass', 'entry-name', field='custom_field') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: secrets stored
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.text.converters 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):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
field = self.get_option('field')
|
||||
|
||||
lp = LPass()
|
||||
|
||||
if not lp.logged_in:
|
||||
raise AnsibleError("Not logged into LastPass: please run 'lpass login' first")
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
values.append(lp.get_field(term, field))
|
||||
return values
|
@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017-2018, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# Copyright (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: lmdb_kv
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens)
|
||||
version_added: '0.2.0'
|
||||
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 U(https://lmdb.readthedocs.io/en/release/))
|
||||
options:
|
||||
_terms:
|
||||
description: List of keys to query.
|
||||
type: list
|
||||
elements: str
|
||||
db:
|
||||
description: Path to LMDB database.
|
||||
type: str
|
||||
default: 'ansible.mdb'
|
||||
vars:
|
||||
- name: lmdb_kv_db
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: query LMDB for a list of country codes
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ query('community.general.lmdb_kv', 'nl', 'be', 'lu', db='jp.mdb') }}"
|
||||
|
||||
- name: use list of values in a loop by key wildcard
|
||||
ansible.builtin.debug:
|
||||
msg: "Hello from {{ item.0 }} a.k.a. {{ item.1 }}"
|
||||
vars:
|
||||
- lmdb_kv_db: jp.mdb
|
||||
with_community.general.lmdb_kv:
|
||||
- "n*"
|
||||
|
||||
- name: get an item by key
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item == 'Belgium'
|
||||
vars:
|
||||
- lmdb_kv_db: jp.mdb
|
||||
with_community.general.lmdb_kv:
|
||||
- be
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: value(s) stored in LMDB
|
||||
type: list
|
||||
elements: raw
|
||||
"""
|
||||
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
HAVE_LMDB = True
|
||||
try:
|
||||
import lmdb
|
||||
except ImportError:
|
||||
HAVE_LMDB = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **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")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
db = self.get_option('db')
|
||||
|
||||
try:
|
||||
env = lmdb.open(str(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
|
@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Arigato Machine Inc.
|
||||
# Copyright (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author:
|
||||
- Kyrylo Galanov (!UNKNOWN) <galanoff@gmail.com>
|
||||
name: 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
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.manifold', api_token='SecretToken') }}"
|
||||
- name: all available resources for a specific project in specific team
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.manifold', api_token='SecretToken', project='poject-1', team='team-2') }}"
|
||||
- name: two specific resources
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.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
|
||||
|
||||
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, **kwargs):
|
||||
"""
|
||||
: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
|
||||
"""
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
api_token = self.get_option('api_token')
|
||||
project = self.get_option('project')
|
||||
team = self.get_option('team')
|
||||
|
||||
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))
|
@ -0,0 +1,239 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Thales Netherlands
|
||||
# Copyright (c) 2021, Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
author:
|
||||
- Roy Lenferink (@rlenferink)
|
||||
- Mark Ettema (@m-a-r-k-e)
|
||||
- Alexander Petrenz (@alpex8)
|
||||
name: merge_variables
|
||||
short_description: merge variables whose names match a given pattern
|
||||
description:
|
||||
- This lookup returns the merged result of all variables in scope that match the given prefixes, suffixes, or
|
||||
regular expressions, optionally.
|
||||
version_added: 6.5.0
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- Depending on the value of O(pattern_type), this is a list of prefixes, suffixes, or regular expressions
|
||||
that will be used to match all variables that should be merged.
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
pattern_type:
|
||||
description:
|
||||
- Change the way of searching for the specified pattern.
|
||||
type: str
|
||||
default: 'regex'
|
||||
choices:
|
||||
- prefix
|
||||
- suffix
|
||||
- regex
|
||||
env:
|
||||
- name: ANSIBLE_MERGE_VARIABLES_PATTERN_TYPE
|
||||
ini:
|
||||
- section: merge_variables_lookup
|
||||
key: pattern_type
|
||||
initial_value:
|
||||
description:
|
||||
- An initial value to start with.
|
||||
type: raw
|
||||
override:
|
||||
description:
|
||||
- Return an error, print a warning or ignore it when a key will be overwritten.
|
||||
- The default behavior V(error) makes the plugin fail when a key would be overwritten.
|
||||
- When V(warn) and V(ignore) are used, note that it is important to know that the variables
|
||||
are sorted by name before being merged. Keys for later variables in this order will overwrite
|
||||
keys of the same name for variables earlier in this order. To avoid potential confusion,
|
||||
better use O(override=error) whenever possible.
|
||||
type: str
|
||||
default: 'error'
|
||||
choices:
|
||||
- error
|
||||
- warn
|
||||
- ignore
|
||||
env:
|
||||
- name: ANSIBLE_MERGE_VARIABLES_OVERRIDE
|
||||
ini:
|
||||
- section: merge_variables_lookup
|
||||
key: override
|
||||
groups:
|
||||
description:
|
||||
- Search for variables accross hosts that belong to the given groups. This allows to collect configuration pieces
|
||||
accross different hosts (for example a service on a host with its database on another host).
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 8.5.0
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Some example variables, they can be defined anywhere as long as they are in scope
|
||||
test_init_list:
|
||||
- "list init item 1"
|
||||
- "list init item 2"
|
||||
|
||||
testa__test_list:
|
||||
- "test a item 1"
|
||||
|
||||
testb__test_list:
|
||||
- "test b item 1"
|
||||
|
||||
testa__test_dict:
|
||||
ports:
|
||||
- 1
|
||||
|
||||
testb__test_dict:
|
||||
ports:
|
||||
- 3
|
||||
|
||||
|
||||
# Merge variables that end with '__test_dict' and store the result in a variable 'example_a'
|
||||
example_a: "{{ lookup('community.general.merge_variables', '__test_dict', pattern_type='suffix') }}"
|
||||
|
||||
# The variable example_a now contains:
|
||||
# ports:
|
||||
# - 1
|
||||
# - 3
|
||||
|
||||
|
||||
# Merge variables that match the '^.+__test_list$' regular expression, starting with an initial value and store the
|
||||
# result in a variable 'example_b'
|
||||
example_b: "{{ lookup('community.general.merge_variables', '^.+__test_list$', initial_value=test_init_list) }}"
|
||||
|
||||
# The variable example_b now contains:
|
||||
# - "list init item 1"
|
||||
# - "list init item 2"
|
||||
# - "test a item 1"
|
||||
# - "test b item 1"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: In case the search matches list items, a list will be returned. In case the search matches dicts, a
|
||||
dict will be returned.
|
||||
type: raw
|
||||
elements: raw
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
def _verify_and_get_type(variable):
|
||||
if isinstance(variable, list):
|
||||
return "list"
|
||||
elif isinstance(variable, dict):
|
||||
return "dict"
|
||||
else:
|
||||
raise AnsibleError("Not supported type detected, variable must be a list or a dict")
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(direct=kwargs)
|
||||
initial_value = self.get_option("initial_value", None)
|
||||
self._override = self.get_option('override', 'error')
|
||||
self._pattern_type = self.get_option('pattern_type', 'regex')
|
||||
self._groups = self.get_option('groups', None)
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
if not isinstance(term, str):
|
||||
raise AnsibleError("Non-string type '{0}' passed, only 'str' types are allowed!".format(type(term)))
|
||||
|
||||
if not self._groups: # consider only own variables
|
||||
ret.append(self._merge_vars(term, initial_value, variables))
|
||||
else: # consider variables of hosts in given groups
|
||||
cross_host_merge_result = initial_value
|
||||
for host in variables["hostvars"]:
|
||||
if self._is_host_in_allowed_groups(variables["hostvars"][host]["group_names"]):
|
||||
host_variables = dict(variables["hostvars"].raw_get(host))
|
||||
host_variables["hostvars"] = variables["hostvars"] # re-add hostvars
|
||||
cross_host_merge_result = self._merge_vars(term, cross_host_merge_result, host_variables)
|
||||
ret.append(cross_host_merge_result)
|
||||
|
||||
return ret
|
||||
|
||||
def _is_host_in_allowed_groups(self, host_groups):
|
||||
if 'all' in self._groups:
|
||||
return True
|
||||
|
||||
group_intersection = [host_group_name for host_group_name in host_groups if host_group_name in self._groups]
|
||||
if group_intersection:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _var_matches(self, key, search_pattern):
|
||||
if self._pattern_type == "prefix":
|
||||
return key.startswith(search_pattern)
|
||||
elif self._pattern_type == "suffix":
|
||||
return key.endswith(search_pattern)
|
||||
elif self._pattern_type == "regex":
|
||||
matcher = re.compile(search_pattern)
|
||||
return matcher.search(key)
|
||||
|
||||
return False
|
||||
|
||||
def _merge_vars(self, search_pattern, initial_value, variables):
|
||||
display.vvv("Merge variables with {0}: {1}".format(self._pattern_type, search_pattern))
|
||||
var_merge_names = sorted([key for key in variables.keys() if self._var_matches(key, search_pattern)])
|
||||
display.vvv("The following variables will be merged: {0}".format(var_merge_names))
|
||||
prev_var_type = None
|
||||
result = None
|
||||
|
||||
if initial_value is not None:
|
||||
prev_var_type = _verify_and_get_type(initial_value)
|
||||
result = initial_value
|
||||
|
||||
for var_name in var_merge_names:
|
||||
with self._templar.set_temporary_context(available_variables=variables): # tmp. switch renderer to context of current variables
|
||||
var_value = self._templar.template(variables[var_name]) # Render jinja2 templates
|
||||
var_type = _verify_and_get_type(var_value)
|
||||
|
||||
if prev_var_type is None:
|
||||
prev_var_type = var_type
|
||||
elif prev_var_type != var_type:
|
||||
raise AnsibleError("Unable to merge, not all variables are of the same type")
|
||||
|
||||
if result is None:
|
||||
result = var_value
|
||||
continue
|
||||
|
||||
if var_type == "dict":
|
||||
result = self._merge_dict(var_value, result, [var_name])
|
||||
else: # var_type == "list"
|
||||
result += var_value
|
||||
|
||||
return result
|
||||
|
||||
def _merge_dict(self, src, dest, path):
|
||||
for key, value in src.items():
|
||||
if isinstance(value, dict):
|
||||
node = dest.setdefault(key, {})
|
||||
self._merge_dict(value, node, path + [key])
|
||||
elif isinstance(value, list) and key in dest:
|
||||
dest[key] += value
|
||||
else:
|
||||
if (key in dest) and dest[key] != value:
|
||||
msg = "The key '{0}' with value '{1}' will be overwritten with value '{2}' from '{3}.{0}'".format(
|
||||
key, dest[key], value, ".".join(path))
|
||||
|
||||
if self._override == "error":
|
||||
raise AnsibleError(msg)
|
||||
if self._override == "warn":
|
||||
display.warning(msg)
|
||||
|
||||
dest[key] = value
|
||||
|
||||
return dest
|
@ -0,0 +1,709 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Scott Buchanan <scott@buchanan.works>
|
||||
# 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: onepassword
|
||||
author:
|
||||
- Scott Buchanan (@scottsb)
|
||||
- Andrew Zenk (@azenk)
|
||||
- Sam Doran (@samdoran)
|
||||
short_description: Fetch field values from 1Password
|
||||
description:
|
||||
- P(community.general.onepassword#lookup) wraps the C(op) command line utility to fetch specific field values from 1Password.
|
||||
requirements:
|
||||
- C(op) 1Password command line utility
|
||||
options:
|
||||
_terms:
|
||||
description: Identifier(s) (case-insensitive UUID or name) of item(s) to retrieve.
|
||||
required: true
|
||||
account_id:
|
||||
version_added: 7.5.0
|
||||
domain:
|
||||
version_added: 3.2.0
|
||||
field:
|
||||
description: Field to return from each matching item (case-insensitive).
|
||||
default: 'password'
|
||||
type: str
|
||||
service_account_token:
|
||||
version_added: 7.1.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.onepassword
|
||||
- community.general.onepassword.lookup
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
# These examples only work when already signed in to 1Password
|
||||
- name: Retrieve password for KITT when already signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword', 'KITT')
|
||||
|
||||
- name: Retrieve password for Wintermute when already signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword', 'Tessier-Ashpool', section='Wintermute')
|
||||
|
||||
- name: Retrieve username for HAL when already signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword', 'HAL 9000', field='username', vault='Discovery')
|
||||
|
||||
- name: Retrieve password for HAL when not signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword',
|
||||
'HAL 9000',
|
||||
subdomain='Discovery',
|
||||
master_password=vault_master_password)
|
||||
|
||||
- name: Retrieve password for HAL when never signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword',
|
||||
'HAL 9000',
|
||||
subdomain='Discovery',
|
||||
master_password=vault_master_password,
|
||||
username='tweety@acme.com',
|
||||
secret_key=vault_secret_key)
|
||||
|
||||
- name: Retrieve password from specific account
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword',
|
||||
'HAL 9000',
|
||||
account_id='abc123')
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: Field data requested.
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
import abc
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.errors import AnsibleLookupError, AnsibleOptionsError
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
from ansible.module_utils.six import with_metaclass
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.onepassword import OnePasswordConfig
|
||||
|
||||
|
||||
def _lower_if_possible(value):
|
||||
"""Return the lower case version value, otherwise return the value"""
|
||||
try:
|
||||
return value.lower()
|
||||
except AttributeError:
|
||||
return value
|
||||
|
||||
|
||||
class OnePassCLIBase(with_metaclass(abc.ABCMeta, object)):
|
||||
bin = "op"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
subdomain=None,
|
||||
domain="1password.com",
|
||||
username=None,
|
||||
secret_key=None,
|
||||
master_password=None,
|
||||
service_account_token=None,
|
||||
account_id=None,
|
||||
connect_host=None,
|
||||
connect_token=None,
|
||||
):
|
||||
self.subdomain = subdomain
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
self.master_password = master_password
|
||||
self.secret_key = secret_key
|
||||
self.service_account_token = service_account_token
|
||||
self.account_id = account_id
|
||||
self.connect_host = connect_host
|
||||
self.connect_token = connect_token
|
||||
|
||||
self._path = None
|
||||
self._version = None
|
||||
|
||||
def _check_required_params(self, required_params):
|
||||
non_empty_attrs = dict((param, getattr(self, param, None)) for param in required_params if getattr(self, param, None))
|
||||
missing = set(required_params).difference(non_empty_attrs)
|
||||
if missing:
|
||||
prefix = "Unable to sign in to 1Password. Missing required parameter"
|
||||
plural = ""
|
||||
suffix = ": {params}.".format(params=", ".join(missing))
|
||||
if len(missing) > 1:
|
||||
plural = "s"
|
||||
|
||||
msg = "{prefix}{plural}{suffix}".format(prefix=prefix, plural=plural, suffix=suffix)
|
||||
raise AnsibleLookupError(msg)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _parse_field(self, data_json, field_name, section_title):
|
||||
"""Main method for parsing data returned from the op command line tool"""
|
||||
|
||||
def _run(self, args, expected_rc=0, command_input=None, ignore_errors=False, environment_update=None):
|
||||
command = [self.path] + args
|
||||
call_kwargs = {
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
"stdin": subprocess.PIPE,
|
||||
}
|
||||
|
||||
if environment_update:
|
||||
env = os.environ.copy()
|
||||
env.update(environment_update)
|
||||
call_kwargs["env"] = env
|
||||
|
||||
p = subprocess.Popen(command, **call_kwargs)
|
||||
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
|
||||
|
||||
@abc.abstractmethod
|
||||
def assert_logged_in(self):
|
||||
"""Check whether a login session exists"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def full_signin(self):
|
||||
"""Performa full login"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_raw(self, item_id, vault=None, token=None):
|
||||
"""Gets the specified item from the vault"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def signin(self):
|
||||
"""Sign in using the master password"""
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if self._path is None:
|
||||
self._path = get_bin_path(self.bin)
|
||||
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
if self._version is None:
|
||||
self._version = self.get_current_version()
|
||||
|
||||
return self._version
|
||||
|
||||
@classmethod
|
||||
def get_current_version(cls):
|
||||
"""Standalone method to get the op CLI version. Useful when determining which class to load
|
||||
based on the current version."""
|
||||
try:
|
||||
bin_path = get_bin_path(cls.bin)
|
||||
except ValueError:
|
||||
raise AnsibleLookupError("Unable to locate '%s' command line tool" % cls.bin)
|
||||
|
||||
try:
|
||||
b_out = subprocess.check_output([bin_path, "--version"], stderr=subprocess.PIPE)
|
||||
except subprocess.CalledProcessError as cpe:
|
||||
raise AnsibleLookupError("Unable to get the op version: %s" % cpe)
|
||||
|
||||
return to_text(b_out).strip()
|
||||
|
||||
|
||||
class OnePassCLIv1(OnePassCLIBase):
|
||||
supports_version = "1"
|
||||
|
||||
def _parse_field(self, data_json, field_name, section_title):
|
||||
"""
|
||||
Retrieves the desired field from the `op` response payload
|
||||
|
||||
When the item is a `password` type, the password is a key within the `details` key:
|
||||
|
||||
$ op get item 'test item' | jq
|
||||
{
|
||||
[...]
|
||||
"templateUuid": "005",
|
||||
"details": {
|
||||
"notesPlain": "",
|
||||
"password": "foobar",
|
||||
"passwordHistory": [],
|
||||
"sections": [
|
||||
{
|
||||
"name": "linked items",
|
||||
"title": "Related Items"
|
||||
}
|
||||
]
|
||||
},
|
||||
[...]
|
||||
}
|
||||
|
||||
However, when the item is a `login` type, the password is within a fields array:
|
||||
|
||||
$ op get item 'test item' | jq
|
||||
{
|
||||
[...]
|
||||
"details": {
|
||||
"fields": [
|
||||
{
|
||||
"designation": "username",
|
||||
"name": "username",
|
||||
"type": "T",
|
||||
"value": "foo"
|
||||
},
|
||||
{
|
||||
"designation": "password",
|
||||
"name": "password",
|
||||
"type": "P",
|
||||
"value": "bar"
|
||||
}
|
||||
],
|
||||
[...]
|
||||
},
|
||||
[...]
|
||||
"""
|
||||
data = json.loads(data_json)
|
||||
if section_title is None:
|
||||
# https://github.com/ansible-collections/community.general/pull/1610:
|
||||
# check the details dictionary for `field_name` and return it immediately if it exists
|
||||
# when the entry is a "password" instead of a "login" item, the password field is a key
|
||||
# in the `details` dictionary:
|
||||
if field_name in data["details"]:
|
||||
return data["details"][field_name]
|
||||
|
||||
# when the field is not found above, iterate through the fields list in the object details
|
||||
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 ""
|
||||
|
||||
def assert_logged_in(self):
|
||||
args = ["get", "account"]
|
||||
if self.account_id:
|
||||
args.extend(["--account", self.account_id])
|
||||
elif self.subdomain:
|
||||
account = "{subdomain}.{domain}".format(subdomain=self.subdomain, domain=self.domain)
|
||||
args.extend(["--account", account])
|
||||
|
||||
rc, out, err = self._run(args, ignore_errors=True)
|
||||
|
||||
return not bool(rc)
|
||||
|
||||
def full_signin(self):
|
||||
if self.connect_host or self.connect_token:
|
||||
raise AnsibleLookupError(
|
||||
"1Password Connect is not available with 1Password CLI version 1. Please use version 2 or later.")
|
||||
|
||||
if self.service_account_token:
|
||||
raise AnsibleLookupError(
|
||||
"1Password CLI version 1 does not support Service Accounts. Please use version 2 or later.")
|
||||
|
||||
required_params = [
|
||||
"subdomain",
|
||||
"username",
|
||||
"secret_key",
|
||||
"master_password",
|
||||
]
|
||||
self._check_required_params(required_params)
|
||||
|
||||
args = [
|
||||
"signin",
|
||||
"{0}.{1}".format(self.subdomain, self.domain),
|
||||
to_bytes(self.username),
|
||||
to_bytes(self.secret_key),
|
||||
"--raw",
|
||||
]
|
||||
|
||||
return self._run(args, command_input=to_bytes(self.master_password))
|
||||
|
||||
def get_raw(self, item_id, vault=None, token=None):
|
||||
args = ["get", "item", item_id]
|
||||
|
||||
if self.account_id:
|
||||
args.extend(["--account", self.account_id])
|
||||
|
||||
if vault is not None:
|
||||
args += ["--vault={0}".format(vault)]
|
||||
|
||||
if token is not None:
|
||||
args += [to_bytes("--session=") + token]
|
||||
|
||||
return self._run(args)
|
||||
|
||||
def signin(self):
|
||||
self._check_required_params(['master_password'])
|
||||
|
||||
args = ["signin", "--raw"]
|
||||
if self.subdomain:
|
||||
args.append(self.subdomain)
|
||||
|
||||
return self._run(args, command_input=to_bytes(self.master_password))
|
||||
|
||||
|
||||
class OnePassCLIv2(OnePassCLIBase):
|
||||
"""
|
||||
CLIv2 Syntax Reference: https://developer.1password.com/docs/cli/upgrade#step-2-update-your-scripts
|
||||
"""
|
||||
supports_version = "2"
|
||||
|
||||
def _parse_field(self, data_json, field_name, section_title=None):
|
||||
"""
|
||||
Schema reference: https://developer.1password.com/docs/cli/item-template-json
|
||||
|
||||
Example Data:
|
||||
|
||||
# Password item
|
||||
{
|
||||
"id": "ywvdbojsguzgrgnokmcxtydgdv",
|
||||
"title": "Authy Backup",
|
||||
"version": 1,
|
||||
"vault": {
|
||||
"id": "bcqxysvcnejjrwzoqrwzcqjqxc",
|
||||
"name": "Personal"
|
||||
},
|
||||
"category": "PASSWORD",
|
||||
"last_edited_by": "7FUPZ8ZNE02KSHMAIMKHIVUE17",
|
||||
"created_at": "2015-01-18T13:13:38Z",
|
||||
"updated_at": "2016-02-20T16:23:54Z",
|
||||
"additional_information": "Jan 18, 2015, 08:13:38",
|
||||
"fields": [
|
||||
{
|
||||
"id": "password",
|
||||
"type": "CONCEALED",
|
||||
"purpose": "PASSWORD",
|
||||
"label": "password",
|
||||
"value": "OctoberPoppyNuttyDraperySabbath",
|
||||
"reference": "op://Personal/Authy Backup/password",
|
||||
"password_details": {
|
||||
"strength": "FANTASTIC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "notesPlain",
|
||||
"type": "STRING",
|
||||
"purpose": "NOTES",
|
||||
"label": "notesPlain",
|
||||
"value": "Backup password to restore Authy",
|
||||
"reference": "op://Personal/Authy Backup/notesPlain"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Login item
|
||||
{
|
||||
"id": "awk4s2u44fhnrgppszcsvc663i",
|
||||
"title": "Dummy Login",
|
||||
"version": 2,
|
||||
"vault": {
|
||||
"id": "stpebbaccrq72xulgouxsk4p7y",
|
||||
"name": "Personal"
|
||||
},
|
||||
"category": "LOGIN",
|
||||
"last_edited_by": "LSGPJERUYBH7BFPHMZ2KKGL6AU",
|
||||
"created_at": "2018-04-25T21:55:19Z",
|
||||
"updated_at": "2018-04-25T21:56:06Z",
|
||||
"additional_information": "agent.smith",
|
||||
"urls": [
|
||||
{
|
||||
"primary": true,
|
||||
"href": "https://acme.com"
|
||||
}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"id": "linked items",
|
||||
"label": "Related Items"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"id": "username",
|
||||
"type": "STRING",
|
||||
"purpose": "USERNAME",
|
||||
"label": "username",
|
||||
"value": "agent.smith",
|
||||
"reference": "op://Personal/Dummy Login/username"
|
||||
},
|
||||
{
|
||||
"id": "password",
|
||||
"type": "CONCEALED",
|
||||
"purpose": "PASSWORD",
|
||||
"label": "password",
|
||||
"value": "Q7vFwTJcqwxKmTU]Dzx7NW*wrNPXmj",
|
||||
"entropy": 159.6083697084228,
|
||||
"reference": "op://Personal/Dummy Login/password",
|
||||
"password_details": {
|
||||
"entropy": 159,
|
||||
"generated": true,
|
||||
"strength": "FANTASTIC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "notesPlain",
|
||||
"type": "STRING",
|
||||
"purpose": "NOTES",
|
||||
"label": "notesPlain",
|
||||
"reference": "op://Personal/Dummy Login/notesPlain"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
data = json.loads(data_json)
|
||||
field_name = _lower_if_possible(field_name)
|
||||
for field in data.get("fields", []):
|
||||
if section_title is None:
|
||||
# If the field name exists in the section, return that value
|
||||
if field.get(field_name):
|
||||
return field.get(field_name)
|
||||
|
||||
# If the field name doesn't exist in the section, match on the value of "label"
|
||||
# then "id" and return "value"
|
||||
if field.get("label", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
if field.get("id", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
# Look at the section data and get an identifier. The value of 'id' is either a unique ID
|
||||
# or a human-readable string. If a 'label' field exists, prefer that since
|
||||
# it is the value visible in the 1Password UI when both 'id' and 'label' exist.
|
||||
section = field.get("section", {})
|
||||
section_title = _lower_if_possible(section_title)
|
||||
|
||||
current_section_title = section.get("label", section.get("id", "")).lower()
|
||||
if section_title == current_section_title:
|
||||
# In the correct section. Check "label" then "id" for the desired field_name
|
||||
if field.get("label", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
if field.get("id", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
return ""
|
||||
|
||||
def assert_logged_in(self):
|
||||
if self.connect_host and self.connect_token:
|
||||
return True
|
||||
|
||||
if self.service_account_token:
|
||||
args = ["whoami"]
|
||||
environment_update = {"OP_SERVICE_ACCOUNT_TOKEN": self.service_account_token}
|
||||
rc, out, err = self._run(args, environment_update=environment_update)
|
||||
|
||||
return not bool(rc)
|
||||
|
||||
args = ["account", "list"]
|
||||
if self.subdomain:
|
||||
account = "{subdomain}.{domain}".format(subdomain=self.subdomain, domain=self.domain)
|
||||
args.extend(["--account", account])
|
||||
|
||||
rc, out, err = self._run(args)
|
||||
|
||||
if out:
|
||||
# Running 'op account get' if there are no accounts configured on the system drops into
|
||||
# an interactive prompt. Only run 'op account get' after first listing accounts to see
|
||||
# if there are any previously configured accounts.
|
||||
args = ["account", "get"]
|
||||
if self.account_id:
|
||||
args.extend(["--account", self.account_id])
|
||||
elif self.subdomain:
|
||||
account = "{subdomain}.{domain}".format(subdomain=self.subdomain, domain=self.domain)
|
||||
args.extend(["--account", account])
|
||||
|
||||
rc, out, err = self._run(args, ignore_errors=True)
|
||||
|
||||
return not bool(rc)
|
||||
|
||||
return False
|
||||
|
||||
def full_signin(self):
|
||||
required_params = [
|
||||
"subdomain",
|
||||
"username",
|
||||
"secret_key",
|
||||
"master_password",
|
||||
]
|
||||
self._check_required_params(required_params)
|
||||
|
||||
args = [
|
||||
"account", "add", "--raw",
|
||||
"--address", "{0}.{1}".format(self.subdomain, self.domain),
|
||||
"--email", to_bytes(self.username),
|
||||
"--signin",
|
||||
]
|
||||
|
||||
environment_update = {"OP_SECRET_KEY": self.secret_key}
|
||||
return self._run(args, command_input=to_bytes(self.master_password), environment_update=environment_update)
|
||||
|
||||
def get_raw(self, item_id, vault=None, token=None):
|
||||
args = ["item", "get", item_id, "--format", "json"]
|
||||
|
||||
if self.account_id:
|
||||
args.extend(["--account", self.account_id])
|
||||
|
||||
if vault is not None:
|
||||
args += ["--vault={0}".format(vault)]
|
||||
|
||||
if self.connect_host and self.connect_token:
|
||||
if vault is None:
|
||||
raise AnsibleLookupError("'vault' is required with 1Password Connect")
|
||||
environment_update = {
|
||||
"OP_CONNECT_HOST": self.connect_host,
|
||||
"OP_CONNECT_TOKEN": self.connect_token,
|
||||
}
|
||||
return self._run(args, environment_update=environment_update)
|
||||
|
||||
if self.service_account_token:
|
||||
if vault is None:
|
||||
raise AnsibleLookupError("'vault' is required with 'service_account_token'")
|
||||
environment_update = {"OP_SERVICE_ACCOUNT_TOKEN": self.service_account_token}
|
||||
return self._run(args, environment_update=environment_update)
|
||||
|
||||
if token is not None:
|
||||
args += [to_bytes("--session=") + token]
|
||||
|
||||
return self._run(args)
|
||||
|
||||
def signin(self):
|
||||
self._check_required_params(['master_password'])
|
||||
|
||||
args = ["signin", "--raw"]
|
||||
if self.subdomain:
|
||||
args.extend(["--account", self.subdomain])
|
||||
|
||||
return self._run(args, command_input=to_bytes(self.master_password))
|
||||
|
||||
|
||||
class OnePass(object):
|
||||
def __init__(self, subdomain=None, domain="1password.com", username=None, secret_key=None, master_password=None,
|
||||
service_account_token=None, account_id=None, connect_host=None, connect_token=None, cli_class=None):
|
||||
self.subdomain = subdomain
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
self.secret_key = secret_key
|
||||
self.master_password = master_password
|
||||
self.service_account_token = service_account_token
|
||||
self.account_id = account_id
|
||||
self.connect_host = connect_host
|
||||
self.connect_token = connect_token
|
||||
|
||||
self.logged_in = False
|
||||
self.token = None
|
||||
|
||||
self._config = OnePasswordConfig()
|
||||
self._cli = self._get_cli_class(cli_class)
|
||||
|
||||
if (self.connect_host or self.connect_token) and None in (self.connect_host, self.connect_token):
|
||||
raise AnsibleOptionsError("connect_host and connect_token are required together")
|
||||
|
||||
def _get_cli_class(self, cli_class=None):
|
||||
if cli_class is not None:
|
||||
return cli_class(self.subdomain, self.domain, self.username, self.secret_key, self.master_password, self.service_account_token)
|
||||
|
||||
version = OnePassCLIBase.get_current_version()
|
||||
for cls in OnePassCLIBase.__subclasses__():
|
||||
if cls.supports_version == version.split(".")[0]:
|
||||
try:
|
||||
return cls(self.subdomain, self.domain, self.username, self.secret_key, self.master_password, self.service_account_token,
|
||||
self.account_id, self.connect_host, self.connect_token)
|
||||
except TypeError as e:
|
||||
raise AnsibleLookupError(e)
|
||||
|
||||
raise AnsibleLookupError("op version %s is unsupported" % version)
|
||||
|
||||
def set_token(self):
|
||||
if self._config.config_file_path and os.path.isfile(self._config.config_file_path):
|
||||
# If the config file exists, assume an initial sign in has taken place and try basic sign in
|
||||
try:
|
||||
rc, out, err = self._cli.signin()
|
||||
except AnsibleLookupError as exc:
|
||||
test_strings = (
|
||||
"missing required parameters",
|
||||
"unauthorized",
|
||||
)
|
||||
if any(string in exc.message.lower() for string in test_strings):
|
||||
# A required parameter is missing, or a bad master password was supplied
|
||||
# so don't bother attempting a full signin
|
||||
raise
|
||||
|
||||
rc, out, err = self._cli.full_signin()
|
||||
|
||||
self.token = out.strip()
|
||||
|
||||
else:
|
||||
# Attempt a full signin since there appears to be no existing signin
|
||||
rc, out, err = self._cli.full_signin()
|
||||
self.token = out.strip()
|
||||
|
||||
def assert_logged_in(self):
|
||||
logged_in = self._cli.assert_logged_in()
|
||||
if logged_in:
|
||||
self.logged_in = logged_in
|
||||
pass
|
||||
else:
|
||||
self.set_token()
|
||||
|
||||
def get_raw(self, item_id, vault=None):
|
||||
rc, out, err = self._cli.get_raw(item_id, vault, self.token)
|
||||
return out
|
||||
|
||||
def get_field(self, item_id, field, section=None, vault=None):
|
||||
output = self.get_raw(item_id, vault)
|
||||
if output:
|
||||
return self._cli._parse_field(output, field, section)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
field = self.get_option("field")
|
||||
section = self.get_option("section")
|
||||
vault = self.get_option("vault")
|
||||
subdomain = self.get_option("subdomain")
|
||||
domain = self.get_option("domain")
|
||||
username = self.get_option("username")
|
||||
secret_key = self.get_option("secret_key")
|
||||
master_password = self.get_option("master_password")
|
||||
service_account_token = self.get_option("service_account_token")
|
||||
account_id = self.get_option("account_id")
|
||||
connect_host = self.get_option("connect_host")
|
||||
connect_token = self.get_option("connect_token")
|
||||
|
||||
op = OnePass(
|
||||
subdomain=subdomain,
|
||||
domain=domain,
|
||||
username=username,
|
||||
secret_key=secret_key,
|
||||
master_password=master_password,
|
||||
service_account_token=service_account_token,
|
||||
account_id=account_id,
|
||||
connect_host=connect_host,
|
||||
connect_token=connect_token,
|
||||
)
|
||||
op.assert_logged_in()
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
values.append(op.get_field(term, field, section, vault))
|
||||
|
||||
return values
|
@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023, Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: onepassword_doc
|
||||
author:
|
||||
- Sam Doran (@samdoran)
|
||||
requirements:
|
||||
- C(op) 1Password command line utility version 2 or later.
|
||||
short_description: Fetch documents stored in 1Password
|
||||
version_added: "8.1.0"
|
||||
description:
|
||||
- P(community.general.onepassword_doc#lookup) wraps C(op) command line utility to fetch one or more documents from 1Password.
|
||||
notes:
|
||||
- The document contents are a string exactly as stored in 1Password.
|
||||
- This plugin requires C(op) version 2 or later.
|
||||
|
||||
options:
|
||||
_terms:
|
||||
description: Identifier(s) (case-insensitive UUID or name) of item(s) to retrieve.
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.onepassword
|
||||
- community.general.onepassword.lookup
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Retrieve a private key from 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword_doc', 'Private key')
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: Requested document
|
||||
type: list
|
||||
elements: string
|
||||
"""
|
||||
|
||||
from ansible_collections.community.general.plugins.lookup.onepassword import OnePass, OnePassCLIv2
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class OnePassCLIv2Doc(OnePassCLIv2):
|
||||
def get_raw(self, item_id, vault=None, token=None):
|
||||
args = ["document", "get", item_id]
|
||||
if vault is not None:
|
||||
args = [*args, "--vault={0}".format(vault)]
|
||||
|
||||
if self.service_account_token:
|
||||
if vault is None:
|
||||
raise AnsibleLookupError("'vault' is required with 'service_account_token'")
|
||||
|
||||
environment_update = {"OP_SERVICE_ACCOUNT_TOKEN": self.service_account_token}
|
||||
return self._run(args, environment_update=environment_update)
|
||||
|
||||
if token is not None:
|
||||
args = [*args, to_bytes("--session=") + token]
|
||||
|
||||
return self._run(args)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
vault = self.get_option("vault")
|
||||
subdomain = self.get_option("subdomain")
|
||||
domain = self.get_option("domain", "1password.com")
|
||||
username = self.get_option("username")
|
||||
secret_key = self.get_option("secret_key")
|
||||
master_password = self.get_option("master_password")
|
||||
service_account_token = self.get_option("service_account_token")
|
||||
account_id = self.get_option("account_id")
|
||||
connect_host = self.get_option("connect_host")
|
||||
connect_token = self.get_option("connect_token")
|
||||
|
||||
op = OnePass(
|
||||
subdomain=subdomain,
|
||||
domain=domain,
|
||||
username=username,
|
||||
secret_key=secret_key,
|
||||
master_password=master_password,
|
||||
service_account_token=service_account_token,
|
||||
account_id=account_id,
|
||||
connect_host=connect_host,
|
||||
connect_token=connect_token,
|
||||
cli_class=OnePassCLIv2Doc,
|
||||
)
|
||||
op.assert_logged_in()
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
values.append(op.get_raw(term, vault))
|
||||
|
||||
return values
|
@ -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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: onepassword_raw
|
||||
author:
|
||||
- Scott Buchanan (@scottsb)
|
||||
- Andrew Zenk (@azenk)
|
||||
- Sam Doran (@samdoran)
|
||||
requirements:
|
||||
- C(op) 1Password command line utility
|
||||
short_description: Fetch an entire item from 1Password
|
||||
description:
|
||||
- P(community.general.onepassword_raw#lookup) wraps C(op) command line utility to fetch an entire item from 1Password.
|
||||
options:
|
||||
_terms:
|
||||
description: Identifier(s) (case-insensitive UUID or name) of item(s) to retrieve.
|
||||
required: true
|
||||
account_id:
|
||||
version_added: 7.5.0
|
||||
domain:
|
||||
version_added: 6.0.0
|
||||
service_account_token:
|
||||
version_added: 7.1.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.onepassword
|
||||
- community.general.onepassword.lookup
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Retrieve all data about Wintermute
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword_raw', 'Wintermute')
|
||||
|
||||
- name: Retrieve all data about Wintermute when not signed in to 1Password
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.onepassword_raw', 'Wintermute', subdomain='Turing', vault_password='DmbslfLvasjdl')
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: Entire item requested.
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
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):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
vault = self.get_option("vault")
|
||||
subdomain = self.get_option("subdomain")
|
||||
domain = self.get_option("domain", "1password.com")
|
||||
username = self.get_option("username")
|
||||
secret_key = self.get_option("secret_key")
|
||||
master_password = self.get_option("master_password")
|
||||
service_account_token = self.get_option("service_account_token")
|
||||
account_id = self.get_option("account_id")
|
||||
connect_host = self.get_option("connect_host")
|
||||
connect_token = self.get_option("connect_token")
|
||||
|
||||
op = OnePass(
|
||||
subdomain=subdomain,
|
||||
domain=domain,
|
||||
username=username,
|
||||
secret_key=secret_key,
|
||||
master_password=master_password,
|
||||
service_account_token=service_account_token,
|
||||
account_id=account_id,
|
||||
connect_host=connect_host,
|
||||
connect_token=connect_token,
|
||||
)
|
||||
op.assert_logged_in()
|
||||
|
||||
values = []
|
||||
for term in terms:
|
||||
data = json.loads(op.get_raw(term, vault))
|
||||
values.append(data)
|
||||
|
||||
return values
|
@ -0,0 +1,541 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Patrick Deelman <patrick@patrickdeelman.nl>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: passwordstore
|
||||
author:
|
||||
- Patrick Deelman (!UNKNOWN) <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.
|
||||
- To avoid problems when accessing multiple secrets at once, add C(auto-expand-secmem) to
|
||||
C(~/.gnupg/gpg-agent.conf). Where this is not possible, consider using O(lock=readwrite) instead.
|
||||
options:
|
||||
_terms:
|
||||
description: query key.
|
||||
required: true
|
||||
directory:
|
||||
description:
|
||||
- The directory of the password store.
|
||||
- If O(backend=pass), the default is V(~/.password-store) is used.
|
||||
- If O(backend=gopass), then the default is the C(path) field in C(~/.config/gopass/config.yml),
|
||||
falling back to V(~/.local/share/gopass/stores/root) if C(path) is not defined in the gopass config.
|
||||
type: path
|
||||
vars:
|
||||
- name: passwordstore
|
||||
env:
|
||||
- name: PASSWORD_STORE_DIR
|
||||
create:
|
||||
description: Create the password if it does not already exist. Takes precedence over O(missing).
|
||||
type: bool
|
||||
default: false
|
||||
overwrite:
|
||||
description: Overwrite the password if it does already exist.
|
||||
type: bool
|
||||
default: false
|
||||
umask:
|
||||
description:
|
||||
- Sets the umask for the created .gpg files. The first octed must be greater than 3 (user readable).
|
||||
- Note pass' default value is V('077').
|
||||
env:
|
||||
- name: PASSWORD_STORE_UMASK
|
||||
version_added: 1.3.0
|
||||
returnall:
|
||||
description: Return all the content of the password, not only the first line.
|
||||
type: bool
|
||||
default: false
|
||||
subkey:
|
||||
description: Return a specific subkey of the password. When set to V(password), always returns the first line.
|
||||
type: str
|
||||
default: password
|
||||
userpass:
|
||||
description: Specify a password to save, instead of a generated one.
|
||||
type: str
|
||||
length:
|
||||
description: The length of the generated password.
|
||||
type: integer
|
||||
default: 16
|
||||
backup:
|
||||
description: Used with O(overwrite=true). Backup the previous password in a subkey.
|
||||
type: bool
|
||||
default: false
|
||||
nosymbols:
|
||||
description: Use alphanumeric characters.
|
||||
type: bool
|
||||
default: false
|
||||
missing:
|
||||
description:
|
||||
- List of preference about what to do if the password file is missing.
|
||||
- If O(create=true), the value for this option is ignored and assumed to be V(create).
|
||||
- If set to V(error), the lookup will error out if the passname does not exist.
|
||||
- If set to V(create), the passname will be created with the provided length O(length) if it does not exist.
|
||||
- If set to V(empty) or V(warn), will return a V(none) in case the passname does not exist.
|
||||
When using C(lookup) and not C(query), this will be translated to an empty string.
|
||||
version_added: 3.1.0
|
||||
type: str
|
||||
default: error
|
||||
choices:
|
||||
- error
|
||||
- warn
|
||||
- empty
|
||||
- create
|
||||
lock:
|
||||
description:
|
||||
- How to synchronize operations.
|
||||
- The default of V(write) only synchronizes write operations.
|
||||
- V(readwrite) synchronizes all operations (including read). This makes sure that gpg-agent is never called in parallel.
|
||||
- V(none) does not do any synchronization.
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: lock
|
||||
type: str
|
||||
default: write
|
||||
choices:
|
||||
- readwrite
|
||||
- write
|
||||
- none
|
||||
version_added: 4.5.0
|
||||
locktimeout:
|
||||
description:
|
||||
- Lock timeout applied when O(lock) is not V(none).
|
||||
- Time with a unit suffix, V(s), V(m), V(h) for seconds, minutes, and hours, respectively. For example, V(900s) equals V(15m).
|
||||
- Correlates with C(pinentry-timeout) in C(~/.gnupg/gpg-agent.conf), see C(man gpg-agent) for details.
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: locktimeout
|
||||
type: str
|
||||
default: 15m
|
||||
version_added: 4.5.0
|
||||
backend:
|
||||
description:
|
||||
- Specify which backend to use.
|
||||
- Defaults to V(pass), passwordstore.org's original pass utility.
|
||||
- V(gopass) support is incomplete.
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: backend
|
||||
vars:
|
||||
- name: passwordstore_backend
|
||||
type: str
|
||||
default: pass
|
||||
choices:
|
||||
- pass
|
||||
- gopass
|
||||
version_added: 5.2.0
|
||||
timestamp:
|
||||
description: Add the password generation information to the end of the file.
|
||||
type: bool
|
||||
default: true
|
||||
version_added: 8.1.0
|
||||
preserve:
|
||||
description: Include the old (edited) password inside the pass file.
|
||||
type: bool
|
||||
default: true
|
||||
version_added: 8.1.0
|
||||
missing_subkey:
|
||||
description:
|
||||
- Preference about what to do if the password subkey is missing.
|
||||
- If set to V(error), the lookup will error out if the subkey does not exist.
|
||||
- If set to V(empty) or V(warn), will return a V(none) in case the subkey does not exist.
|
||||
version_added: 8.6.0
|
||||
type: str
|
||||
default: empty
|
||||
choices:
|
||||
- error
|
||||
- warn
|
||||
- empty
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: missing_subkey
|
||||
notes:
|
||||
- The lookup supports passing all options as lookup parameters since community.general 6.0.0.
|
||||
'''
|
||||
EXAMPLES = """
|
||||
ansible.cfg: |
|
||||
[passwordstore_lookup]
|
||||
lock=readwrite
|
||||
locktimeout=45s
|
||||
missing_subkey=warn
|
||||
|
||||
tasks.yml: |
|
||||
---
|
||||
|
||||
# Debug is used for examples, BAD IDEA to show passwords on screen
|
||||
- name: Basic lookup. Fails if example/test does not exist
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test')}}"
|
||||
|
||||
- name: Basic lookup. Warns if example/test does not exist and returns empty string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test', missing='warn')}}"
|
||||
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test', create=true)}}"
|
||||
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test', missing='create')}}"
|
||||
|
||||
- name: Prints 'abc' if example/test does not exist, just give the password otherwise
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: >-
|
||||
{{ lookup('community.general.passwordstore', 'example/test', missing='empty')
|
||||
| default('abc', true) }}
|
||||
|
||||
- name: Different size password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.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
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test', create=true, overwrite=true)}}"
|
||||
|
||||
- name: Create an alphanumeric password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test', create=true, nosymbols=true) }}"
|
||||
|
||||
- name: Return the value for user in the KV pair user, username
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test', subkey='user')}}"
|
||||
|
||||
- name: Return the entire password file content
|
||||
ansible.builtin.set_fact:
|
||||
passfilecontent: "{{ lookup('community.general.passwordstore', 'example/test', returnall=true)}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- a password
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.encrypt import random_password
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible import constants as C
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils._filelock import FileLock
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
# backhacked check_output with input for python 2.7
|
||||
# http://stackoverflow.com/questions/10103551/passing-data-to-subprocess-check-output
|
||||
# note: contains special logic for calling 'pass', so not a drop-in replacement for 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 and (b'encryption failed: Unusable public key' in b_out or
|
||||
b'encryption failed: Unusable public key' in b_err):
|
||||
retcode = 78 # os.EX_CONFIG
|
||||
if retcode != 0:
|
||||
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 __init__(self, loader=None, templar=None, **kwargs):
|
||||
|
||||
super(LookupModule, self).__init__(loader, templar, **kwargs)
|
||||
self.realpass = None
|
||||
|
||||
def is_real_pass(self):
|
||||
if self.realpass is None:
|
||||
try:
|
||||
passoutput = to_text(
|
||||
check_output2([self.pass_cmd, "--version"], env=self.env),
|
||||
errors='surrogate_or_strict'
|
||||
)
|
||||
self.realpass = 'pass: the standard unix password manager' in passoutput
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
raise AnsibleError('exit code {0} while running {1}. Error output: {2}'.format(e.returncode, e.cmd, e.output))
|
||||
|
||||
return self.realpass
|
||||
|
||||
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('=', 1)
|
||||
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] = boolean(self.paramvals[key])
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
if self.paramvals['missing'] not in ['error', 'warn', 'create', 'empty']:
|
||||
raise AnsibleError("{0} is not a valid option for missing".format(self.paramvals['missing']))
|
||||
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']))
|
||||
|
||||
if self.paramvals['create']:
|
||||
self.paramvals['missing'] = 'create'
|
||||
|
||||
# Collect pass environment variables from the plugin's parameters.
|
||||
self.env = os.environ.copy()
|
||||
self.env['LANGUAGE'] = 'C' # make sure to get errors in English as required by check_output2
|
||||
|
||||
if self.backend == 'gopass':
|
||||
self.env['GOPASS_NO_REMINDER'] = "YES"
|
||||
elif os.path.isdir(self.paramvals['directory']):
|
||||
# Set PASSWORD_STORE_DIR
|
||||
self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory']
|
||||
elif self.is_real_pass():
|
||||
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
|
||||
|
||||
# Set PASSWORD_STORE_UMASK if umask is set
|
||||
if self.paramvals.get('umask') is not None:
|
||||
if len(self.paramvals['umask']) != 3:
|
||||
raise AnsibleError('Passwordstore umask must have a length of 3.')
|
||||
elif int(self.paramvals['umask'][0]) > 3:
|
||||
raise AnsibleError('Passwordstore umask not allowed (password not user readable).')
|
||||
else:
|
||||
self.env['PASSWORD_STORE_UMASK'] = self.paramvals['umask']
|
||||
|
||||
def check_pass(self):
|
||||
try:
|
||||
self.passoutput = to_text(
|
||||
check_output2([self.pass_cmd, 'show'] +
|
||||
[self.passname], env=self.env),
|
||||
errors='surrogate_or_strict'
|
||||
).splitlines()
|
||||
self.password = self.passoutput[0]
|
||||
self.passdict = {}
|
||||
try:
|
||||
values = yaml.safe_load('\n'.join(self.passoutput[1:]))
|
||||
for key, item in values.items():
|
||||
self.passdict[key] = item
|
||||
except (yaml.YAMLError, AttributeError):
|
||||
for line in self.passoutput[1:]:
|
||||
if ':' in line:
|
||||
name, value = line.split(':', 1)
|
||||
self.passdict[name.strip()] = value.strip()
|
||||
if (self.backend == 'gopass' or
|
||||
os.path.isfile(os.path.join(self.paramvals['directory'], self.passname + ".gpg"))
|
||||
or not self.is_real_pass()):
|
||||
# When using real pass, only accept password as found if there is a .gpg file for it (might be a tree node otherwise)
|
||||
return True
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
# 'not in password store' is the expected error if a password wasn't found
|
||||
if 'not in the password store' not in e.output:
|
||||
raise AnsibleError('exit code {0} while running {1}. Error output: {2}'.format(e.returncode, e.cmd, e.output))
|
||||
|
||||
if self.paramvals['missing'] == 'error':
|
||||
raise AnsibleError('passwordstore: passname {0} not found and missing=error is set'.format(self.passname))
|
||||
elif self.paramvals['missing'] == 'warn':
|
||||
display.warning('passwordstore: passname {0} not found'.format(self.passname))
|
||||
|
||||
return False
|
||||
|
||||
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
|
||||
if self.paramvals['preserve'] or self.paramvals['timestamp']:
|
||||
msg += '\n'
|
||||
if self.paramvals['preserve'] and self.passoutput[1:]:
|
||||
msg += '\n'.join(self.passoutput[1:]) + '\n'
|
||||
if self.paramvals['timestamp'] and self.paramvals['backup']:
|
||||
msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime)
|
||||
try:
|
||||
check_output2([self.pass_cmd, 'insert', '-f', '-m', self.passname], input=msg, env=self.env)
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
raise AnsibleError('exit code {0} while running {1}. Error output: {2}'.format(e.returncode, e.cmd, e.output))
|
||||
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
|
||||
if self.paramvals['timestamp']:
|
||||
msg += '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime)
|
||||
try:
|
||||
check_output2([self.pass_cmd, 'insert', '-f', '-m', self.passname], input=msg, env=self.env)
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
raise AnsibleError('exit code {0} while running {1}. Error output: {2}'.format(e.returncode, e.cmd, e.output))
|
||||
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:
|
||||
if self.paramvals["missing_subkey"] == "error":
|
||||
raise AnsibleError(
|
||||
"passwordstore: subkey {0} for passname {1} not found and missing_subkey=error is set".format(
|
||||
self.paramvals["subkey"], self.passname
|
||||
)
|
||||
)
|
||||
|
||||
if self.paramvals["missing_subkey"] == "warn":
|
||||
display.warning(
|
||||
"passwordstore: subkey {0} for passname {1} not found".format(
|
||||
self.paramvals["subkey"], self.passname
|
||||
)
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@contextmanager
|
||||
def opt_lock(self, type):
|
||||
if self.get_option('lock') == type:
|
||||
tmpdir = os.environ.get('TMPDIR', '/tmp')
|
||||
lockfile = os.path.join(tmpdir, '.passwordstore.lock')
|
||||
with FileLock().lock_file(lockfile, tmpdir, self.lock_timeout):
|
||||
self.locked = type
|
||||
yield
|
||||
self.locked = None
|
||||
else:
|
||||
yield
|
||||
|
||||
def setup(self, variables):
|
||||
self.backend = self.get_option('backend')
|
||||
self.pass_cmd = self.backend # pass and gopass are commands as well
|
||||
self.locked = None
|
||||
timeout = self.get_option('locktimeout')
|
||||
if not re.match('^[0-9]+[smh]$', timeout):
|
||||
raise AnsibleError("{0} is not a correct value for locktimeout".format(timeout))
|
||||
unit_to_seconds = {"s": 1, "m": 60, "h": 3600}
|
||||
self.lock_timeout = int(timeout[:-1]) * unit_to_seconds[timeout[-1]]
|
||||
|
||||
directory = self.get_option('directory')
|
||||
if directory is None:
|
||||
if self.backend == 'gopass':
|
||||
try:
|
||||
with open(os.path.expanduser('~/.config/gopass/config.yml')) as f:
|
||||
directory = yaml.safe_load(f)['path']
|
||||
except (FileNotFoundError, KeyError, yaml.YAMLError):
|
||||
directory = os.path.expanduser('~/.local/share/gopass/stores/root')
|
||||
else:
|
||||
directory = os.path.expanduser('~/.password-store')
|
||||
|
||||
self.paramvals = {
|
||||
'subkey': self.get_option('subkey'),
|
||||
'directory': directory,
|
||||
'create': self.get_option('create'),
|
||||
'returnall': self.get_option('returnall'),
|
||||
'overwrite': self.get_option('overwrite'),
|
||||
'nosymbols': self.get_option('nosymbols'),
|
||||
'userpass': self.get_option('userpass') or '',
|
||||
'length': self.get_option('length'),
|
||||
'backup': self.get_option('backup'),
|
||||
'missing': self.get_option('missing'),
|
||||
'umask': self.get_option('umask'),
|
||||
'timestamp': self.get_option('timestamp'),
|
||||
'preserve': self.get_option('preserve'),
|
||||
"missing_subkey": self.get_option("missing_subkey"),
|
||||
}
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
self.setup(variables)
|
||||
result = []
|
||||
|
||||
for term in terms:
|
||||
self.parse_params(term) # parse the input into paramvals
|
||||
with self.opt_lock('readwrite'):
|
||||
if self.check_pass(): # password exists
|
||||
if self.paramvals['overwrite'] and self.paramvals['subkey'] == 'password':
|
||||
with self.opt_lock('write'):
|
||||
result.append(self.update_password())
|
||||
else:
|
||||
result.append(self.get_passresult())
|
||||
else: # password does not exist
|
||||
if self.paramvals['missing'] == 'create':
|
||||
with self.opt_lock('write'):
|
||||
if self.locked == 'write' and self.check_pass(): # lookup password again if under write lock
|
||||
result.append(self.get_passresult())
|
||||
else:
|
||||
result.append(self.generate_password())
|
||||
else:
|
||||
result.append(None)
|
||||
|
||||
return result
|
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Abhijeet Kasurde <akasurde@redhat.com>
|
||||
# Copyright (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
name: random_pet
|
||||
author:
|
||||
- Abhijeet Kasurde (@Akasurde)
|
||||
short_description: Generates random pet names
|
||||
version_added: '3.1.0'
|
||||
requirements:
|
||||
- petname U(https://github.com/dustinkirkland/python-petname)
|
||||
description:
|
||||
- Generates random pet names that can be used as unique identifiers for the resources.
|
||||
options:
|
||||
words:
|
||||
description:
|
||||
- The number of words in the pet name.
|
||||
default: 2
|
||||
type: int
|
||||
length:
|
||||
description:
|
||||
- The maximal length of every component of the pet name.
|
||||
- Values below 3 will be set to 3 by petname.
|
||||
default: 6
|
||||
type: int
|
||||
prefix:
|
||||
description: A string to prefix with the name.
|
||||
type: str
|
||||
separator:
|
||||
description: The character to separate words in the pet name.
|
||||
default: "-"
|
||||
type: str
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Generate pet name
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_pet')
|
||||
# Example result: 'loving-raptor'
|
||||
|
||||
- name: Generate pet name with 3 words
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_pet', words=3)
|
||||
# Example result: 'fully-fresh-macaw'
|
||||
|
||||
- name: Generate pet name with separator
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_pet', separator="_")
|
||||
# Example result: 'causal_snipe'
|
||||
|
||||
- name: Generate pet name with length
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_pet', length=7)
|
||||
# Example result: 'natural-peacock'
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
_raw:
|
||||
description: A one-element list containing a random pet name
|
||||
type: list
|
||||
elements: str
|
||||
'''
|
||||
|
||||
try:
|
||||
import petname
|
||||
|
||||
HAS_PETNAME = True
|
||||
except ImportError:
|
||||
HAS_PETNAME = False
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if not HAS_PETNAME:
|
||||
raise AnsibleError('Python petname library is required. '
|
||||
'Please install using "pip install petname"')
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
words = self.get_option('words')
|
||||
length = self.get_option('length')
|
||||
prefix = self.get_option('prefix')
|
||||
separator = self.get_option('separator')
|
||||
|
||||
values = petname.Generate(words=words, separator=separator, letters=length)
|
||||
if prefix:
|
||||
values = "%s%s%s" % (prefix, separator, values)
|
||||
|
||||
return [values]
|
@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Abhijeet Kasurde <akasurde@redhat.com>
|
||||
# Copyright (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: random_string
|
||||
author:
|
||||
- Abhijeet Kasurde (@Akasurde)
|
||||
short_description: Generates random string
|
||||
version_added: '3.2.0'
|
||||
description:
|
||||
- Generates random string based upon the given constraints.
|
||||
- Uses L(random.SystemRandom,https://docs.python.org/3/library/random.html#random.SystemRandom),
|
||||
so should be strong enough for cryptographic purposes.
|
||||
options:
|
||||
length:
|
||||
description: The length of the string.
|
||||
default: 8
|
||||
type: int
|
||||
upper:
|
||||
description:
|
||||
- Include uppercase letters in the string.
|
||||
default: true
|
||||
type: bool
|
||||
lower:
|
||||
description:
|
||||
- Include lowercase letters in the string.
|
||||
default: true
|
||||
type: bool
|
||||
numbers:
|
||||
description:
|
||||
- Include numbers in the string.
|
||||
default: true
|
||||
type: bool
|
||||
special:
|
||||
description:
|
||||
- Include special characters in the string.
|
||||
- Special characters are taken from Python standard library C(string).
|
||||
See L(the documentation of string.punctuation,https://docs.python.org/3/library/string.html#string.punctuation)
|
||||
for which characters will be used.
|
||||
- The choice of special characters can be changed to setting O(override_special).
|
||||
default: true
|
||||
type: bool
|
||||
min_numeric:
|
||||
description:
|
||||
- Minimum number of numeric characters in the string.
|
||||
- If set, overrides O(numbers=false).
|
||||
default: 0
|
||||
type: int
|
||||
min_upper:
|
||||
description:
|
||||
- Minimum number of uppercase alphabets in the string.
|
||||
- If set, overrides O(upper=false).
|
||||
default: 0
|
||||
type: int
|
||||
min_lower:
|
||||
description:
|
||||
- Minimum number of lowercase alphabets in the string.
|
||||
- If set, overrides O(lower=false).
|
||||
default: 0
|
||||
type: int
|
||||
min_special:
|
||||
description:
|
||||
- Minimum number of special character in the string.
|
||||
default: 0
|
||||
type: int
|
||||
override_special:
|
||||
description:
|
||||
- Override a list of special characters to use in the string.
|
||||
- If set O(min_special) should be set to a non-default value.
|
||||
type: str
|
||||
override_all:
|
||||
description:
|
||||
- Override all values of O(numbers), O(upper), O(lower), and O(special) with
|
||||
the given list of characters.
|
||||
type: str
|
||||
ignore_similar_chars:
|
||||
description:
|
||||
- Ignore similar characters, such as V(l) and V(1), or V(O) and V(0).
|
||||
- These characters can be configured in O(similar_chars).
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 7.5.0
|
||||
similar_chars:
|
||||
description:
|
||||
- Override a list of characters not to be use in the string.
|
||||
default: "il1LoO0"
|
||||
type: str
|
||||
version_added: 7.5.0
|
||||
base64:
|
||||
description:
|
||||
- Returns base64 encoded string.
|
||||
type: bool
|
||||
default: false
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Generate random string
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_string')
|
||||
# Example result: ['DeadBeeF']
|
||||
|
||||
- name: Generate random string with length 12
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_string', length=12)
|
||||
# Example result: ['Uan0hUiX5kVG']
|
||||
|
||||
- name: Generate base64 encoded random string
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_string', base64=True)
|
||||
# Example result: ['NHZ6eWN5Qk0=']
|
||||
|
||||
- name: Generate a random string with 1 lower, 1 upper, 1 number and 1 special char (at least)
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_string', min_lower=1, min_upper=1, min_special=1, min_numeric=1)
|
||||
# Example result: ['&Qw2|E[-']
|
||||
|
||||
- name: Generate a random string with all lower case characters
|
||||
debug:
|
||||
var: query('community.general.random_string', upper=false, numbers=false, special=false)
|
||||
# Example result: ['exolxzyz']
|
||||
|
||||
- name: Generate random hexadecimal string
|
||||
debug:
|
||||
var: query('community.general.random_string', upper=false, lower=false, override_special=hex_chars, numbers=false)
|
||||
vars:
|
||||
hex_chars: '0123456789ABCDEF'
|
||||
# Example result: ['D2A40737']
|
||||
|
||||
- name: Generate random hexadecimal string with override_all
|
||||
debug:
|
||||
var: query('community.general.random_string', override_all=hex_chars)
|
||||
vars:
|
||||
hex_chars: '0123456789ABCDEF'
|
||||
# Example result: ['D2A40737']
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_raw:
|
||||
description: A one-element list containing a random string
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
@staticmethod
|
||||
def get_random(random_generator, chars, length):
|
||||
if not chars:
|
||||
raise AnsibleLookupError(
|
||||
"Available characters cannot be None, please change constraints"
|
||||
)
|
||||
return "".join(random_generator.choice(chars) for dummy in range(length))
|
||||
|
||||
@staticmethod
|
||||
def b64encode(string_value, encoding="utf-8"):
|
||||
return to_text(
|
||||
base64.b64encode(
|
||||
to_bytes(string_value, encoding=encoding, errors="surrogate_or_strict")
|
||||
)
|
||||
)
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
number_chars = string.digits
|
||||
lower_chars = string.ascii_lowercase
|
||||
upper_chars = string.ascii_uppercase
|
||||
special_chars = string.punctuation
|
||||
random_generator = random.SystemRandom()
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
length = self.get_option("length")
|
||||
base64_flag = self.get_option("base64")
|
||||
override_all = self.get_option("override_all")
|
||||
ignore_similar_chars = self.get_option("ignore_similar_chars")
|
||||
similar_chars = self.get_option("similar_chars")
|
||||
values = ""
|
||||
available_chars_set = ""
|
||||
|
||||
if ignore_similar_chars:
|
||||
number_chars = "".join([sc for sc in number_chars if sc not in similar_chars])
|
||||
lower_chars = "".join([sc for sc in lower_chars if sc not in similar_chars])
|
||||
upper_chars = "".join([sc for sc in upper_chars if sc not in similar_chars])
|
||||
special_chars = "".join([sc for sc in special_chars if sc not in similar_chars])
|
||||
|
||||
if override_all:
|
||||
# Override all the values
|
||||
available_chars_set = override_all
|
||||
else:
|
||||
upper = self.get_option("upper")
|
||||
lower = self.get_option("lower")
|
||||
numbers = self.get_option("numbers")
|
||||
special = self.get_option("special")
|
||||
override_special = self.get_option("override_special")
|
||||
|
||||
if override_special:
|
||||
special_chars = override_special
|
||||
|
||||
if upper:
|
||||
available_chars_set += upper_chars
|
||||
if lower:
|
||||
available_chars_set += lower_chars
|
||||
if numbers:
|
||||
available_chars_set += number_chars
|
||||
if special:
|
||||
available_chars_set += special_chars
|
||||
|
||||
mapping = {
|
||||
"min_numeric": number_chars,
|
||||
"min_lower": lower_chars,
|
||||
"min_upper": upper_chars,
|
||||
"min_special": special_chars,
|
||||
}
|
||||
|
||||
for m in mapping:
|
||||
if self.get_option(m):
|
||||
values += self.get_random(random_generator, mapping[m], self.get_option(m))
|
||||
|
||||
remaining_pass_len = length - len(values)
|
||||
values += self.get_random(random_generator, available_chars_set, remaining_pass_len)
|
||||
|
||||
# Get pseudo randomization
|
||||
shuffled_values = list(values)
|
||||
# Randomize the order
|
||||
random.shuffle(shuffled_values)
|
||||
|
||||
if base64_flag:
|
||||
return [self.b64encode("".join(shuffled_values))]
|
||||
|
||||
return ["".join(shuffled_values)]
|
@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""The community.general.random_words Ansible lookup plugin."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: random_words
|
||||
author:
|
||||
- Thomas Sjögren (@konstruktoid)
|
||||
short_description: Return a number of random words
|
||||
version_added: "4.0.0"
|
||||
requirements:
|
||||
- xkcdpass U(https://github.com/redacted/XKCD-password-generator)
|
||||
description:
|
||||
- Returns a number of random words. The output can for example be used for
|
||||
passwords.
|
||||
- See U(https://xkcd.com/936/) for background.
|
||||
options:
|
||||
numwords:
|
||||
description:
|
||||
- The number of words.
|
||||
default: 6
|
||||
type: int
|
||||
min_length:
|
||||
description:
|
||||
- Minimum length of words to make password.
|
||||
default: 5
|
||||
type: int
|
||||
max_length:
|
||||
description:
|
||||
- Maximum length of words to make password.
|
||||
default: 9
|
||||
type: int
|
||||
delimiter:
|
||||
description:
|
||||
- The delimiter character between words.
|
||||
default: " "
|
||||
type: str
|
||||
case:
|
||||
description:
|
||||
- The method for setting the case of each word in the passphrase.
|
||||
choices: ["alternating", "upper", "lower", "random", "capitalize"]
|
||||
default: "lower"
|
||||
type: str
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Generate password with default settings
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_words')
|
||||
# Example result: 'traitor gigabyte cesarean unless aspect clear'
|
||||
|
||||
- name: Generate password with six, five character, words
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_words', min_length=5, max_length=5)
|
||||
# Example result: 'brink banjo getup staff trump comfy'
|
||||
|
||||
- name: Generate password with three capitalized words and the '-' delimiter
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_words', numwords=3, delimiter='-', case='capitalize')
|
||||
# Example result: 'Overlabor-Faucet-Coastline'
|
||||
|
||||
- name: Generate password with three words without any delimiter
|
||||
ansible.builtin.debug:
|
||||
var: lookup('community.general.random_words', numwords=3, delimiter='')
|
||||
# Example result: 'deskworkmonopolystriking'
|
||||
# https://www.ncsc.gov.uk/blog-post/the-logic-behind-three-random-words
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_raw:
|
||||
description: A single-element list containing random words.
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
try:
|
||||
from xkcdpass import xkcd_password as xp
|
||||
|
||||
HAS_XKCDPASS = True
|
||||
except ImportError:
|
||||
HAS_XKCDPASS = False
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""The random_words Ansible lookup class."""
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if not HAS_XKCDPASS:
|
||||
raise AnsibleLookupError(
|
||||
"Python xkcdpass library is required. "
|
||||
'Please install using "pip install xkcdpass"'
|
||||
)
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
method = self.get_option("case")
|
||||
delimiter = self.get_option("delimiter")
|
||||
max_length = self.get_option("max_length")
|
||||
min_length = self.get_option("min_length")
|
||||
numwords = self.get_option("numwords")
|
||||
|
||||
words = xp.locate_wordfile()
|
||||
wordlist = xp.generate_wordlist(
|
||||
max_length=max_length, min_length=min_length, wordfile=words
|
||||
)
|
||||
|
||||
values = xp.generate_xkcdpassword(
|
||||
wordlist, case=method, delimiter=delimiter, numwords=numwords
|
||||
)
|
||||
|
||||
return [values]
|
@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2012, Jan-Piet Mens <jpmens(at)gmail.com>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: redis
|
||||
author:
|
||||
- Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com>
|
||||
- Ansible Core Team
|
||||
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)
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.redis', 'somekey') }}"
|
||||
|
||||
- name: query redis for list of keys and non-default host and port
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.redis', item, host='myredis.internal.com', port=2121) }}"
|
||||
loop: '{{list_of_redis_keys}}'
|
||||
|
||||
- name: use list directly
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.redis', 'key1', 'key2', 'key3') }}"
|
||||
|
||||
- name: use list directly with a socket
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.redis', 'key1', 'key2', socket='/var/tmp/redis.sock') }}"
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description: value(s) stored in Redis
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
HAVE_REDIS = False
|
||||
try:
|
||||
import redis
|
||||
HAVE_REDIS = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.module_utils.common.text.converters 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
|
@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, RevBits <info@revbits.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: revbitspss
|
||||
author: RevBits (@RevBits) <info@revbits.com>
|
||||
short_description: Get secrets from RevBits PAM server
|
||||
version_added: 4.1.0
|
||||
description:
|
||||
- Uses the revbits_ansible Python SDK to get Secrets from RevBits PAM
|
||||
Server using API key authentication with the REST API.
|
||||
requirements:
|
||||
- revbits_ansible - U(https://pypi.org/project/revbits_ansible/)
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- This will be an array of keys for secrets which you want to fetch from RevBits PAM.
|
||||
required: true
|
||||
type: list
|
||||
elements: string
|
||||
base_url:
|
||||
description:
|
||||
- This will be the base URL of the server, for example V(https://server-url-here).
|
||||
required: true
|
||||
type: string
|
||||
api_key:
|
||||
description:
|
||||
- This will be the API key for authentication. You can get it from the RevBits PAM secret manager module.
|
||||
required: true
|
||||
type: string
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_list:
|
||||
description:
|
||||
- The JSON responses which you can access with defined keys.
|
||||
- If you are fetching secrets named as UUID, PASSWORD it will gives you the dict of all secrets.
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.revbitspss',
|
||||
'UUIDPAM', 'DB_PASS',
|
||||
base_url='https://server-url-here',
|
||||
api_key='API_KEY_GOES_HERE'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
UUIDPAM is {{ (secret['UUIDPAM']) }} and DB_PASS is {{ (secret['DB_PASS']) }}
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.six import raise_from
|
||||
|
||||
try:
|
||||
from pam.revbits_ansible.server import SecretServer
|
||||
except ImportError as imp_exc:
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR = imp_exc
|
||||
else:
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR = None
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
@staticmethod
|
||||
def Client(server_parameters):
|
||||
return SecretServer(**server_parameters)
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
if ANOTHER_LIBRARY_IMPORT_ERROR:
|
||||
raise_from(
|
||||
AnsibleError('revbits_ansible must be installed to use this plugin'),
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR
|
||||
)
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
secret_server = LookupModule.Client(
|
||||
{
|
||||
"base_url": self.get_option('base_url'),
|
||||
"api_key": self.get_option('api_key'),
|
||||
}
|
||||
)
|
||||
result = []
|
||||
for term in terms:
|
||||
try:
|
||||
display.vvv("Secret Server lookup of Secret with ID %s" % term)
|
||||
result.append({term: secret_server.get_pam_secret(term)})
|
||||
except Exception as error:
|
||||
raise AnsibleError("Secret Server lookup failure: %s" % error.message)
|
||||
return result
|
@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Alejandro Guirao <lekumberri@gmail.com>
|
||||
# Copyright (c) 2012-17 Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: shelvefile
|
||||
author: Alejandro Guirao (!UNKNOWN) <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
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.shelvefile', 'file=path_to_some_shelve_file.db key=key_to_retrieve') }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_list:
|
||||
description: Value(s) of key(s) in shelve file(s).
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
import shelve
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.text.converters 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
|
@ -0,0 +1,442 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Adam Migus <adam@migus.org>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: tss
|
||||
author: Adam Migus (@amigus) <adam@migus.org>
|
||||
short_description: Get secrets from Thycotic Secret Server
|
||||
version_added: 1.0.0
|
||||
description:
|
||||
- Uses the Thycotic Secret Server Python SDK to get Secrets from Secret
|
||||
Server using token authentication with O(username) and O(password) on
|
||||
the REST API at O(base_url).
|
||||
- When using self-signed certificates the environment variable
|
||||
E(REQUESTS_CA_BUNDLE) can be set to a file containing the trusted certificates
|
||||
(in C(.pem) format).
|
||||
- For example, C(export REQUESTS_CA_BUNDLE='/etc/ssl/certs/ca-bundle.trust.crt').
|
||||
requirements:
|
||||
- python-tss-sdk - https://pypi.org/project/python-tss-sdk/
|
||||
options:
|
||||
_terms:
|
||||
description: The integer ID of the secret.
|
||||
required: true
|
||||
type: int
|
||||
secret_path:
|
||||
description: Indicate a full path of secret including folder and secret name when the secret ID is set to 0.
|
||||
required: false
|
||||
type: str
|
||||
version_added: 7.2.0
|
||||
fetch_secret_ids_from_folder:
|
||||
description:
|
||||
- Boolean flag which indicates whether secret ids are in a folder is fetched by folder ID or not.
|
||||
- V(true) then the terms will be considered as a folder IDs. Otherwise (default), they are considered as secret IDs.
|
||||
required: false
|
||||
type: bool
|
||||
version_added: 7.1.0
|
||||
fetch_attachments:
|
||||
description:
|
||||
- Boolean flag which indicates whether attached files will get downloaded or not.
|
||||
- The download will only happen if O(file_download_path) has been provided.
|
||||
required: false
|
||||
type: bool
|
||||
version_added: 7.0.0
|
||||
file_download_path:
|
||||
description: Indicate the file attachment download location.
|
||||
required: false
|
||||
type: path
|
||||
version_added: 7.0.0
|
||||
base_url:
|
||||
description: The base URL of the server, for example V(https://localhost/SecretServer).
|
||||
env:
|
||||
- name: TSS_BASE_URL
|
||||
ini:
|
||||
- section: tss_lookup
|
||||
key: base_url
|
||||
required: true
|
||||
username:
|
||||
description: The username with which to request the OAuth2 Access Grant.
|
||||
env:
|
||||
- name: TSS_USERNAME
|
||||
ini:
|
||||
- section: tss_lookup
|
||||
key: username
|
||||
password:
|
||||
description:
|
||||
- The password associated with the supplied username.
|
||||
- Required when O(token) is not provided.
|
||||
env:
|
||||
- name: TSS_PASSWORD
|
||||
ini:
|
||||
- section: tss_lookup
|
||||
key: password
|
||||
domain:
|
||||
default: ""
|
||||
description:
|
||||
- The domain with which to request the OAuth2 Access Grant.
|
||||
- Optional when O(token) is not provided.
|
||||
- Requires C(python-tss-sdk) version 1.0.0 or greater.
|
||||
env:
|
||||
- name: TSS_DOMAIN
|
||||
ini:
|
||||
- section: tss_lookup
|
||||
key: domain
|
||||
required: false
|
||||
version_added: 3.6.0
|
||||
token:
|
||||
description:
|
||||
- Existing token for Thycotic authorizer.
|
||||
- If provided, O(username) and O(password) are not needed.
|
||||
- Requires C(python-tss-sdk) version 1.0.0 or greater.
|
||||
env:
|
||||
- name: TSS_TOKEN
|
||||
ini:
|
||||
- section: tss_lookup
|
||||
key: token
|
||||
version_added: 3.7.0
|
||||
api_path_uri:
|
||||
default: /api/v1
|
||||
description: The path to append to the base URL to form a valid REST
|
||||
API request.
|
||||
env:
|
||||
- name: TSS_API_PATH_URI
|
||||
required: false
|
||||
token_path_uri:
|
||||
default: /oauth2/token
|
||||
description: The path to append to the base URL to form a valid OAuth2
|
||||
Access Grant request.
|
||||
env:
|
||||
- name: TSS_TOKEN_PATH_URI
|
||||
required: false
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_list:
|
||||
description:
|
||||
- The JSON responses to C(GET /secrets/{id}).
|
||||
- See U(https://updates.thycotic.net/secretserver/restapiguide/TokenAuth/#operation--secrets--id--get).
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.tss',
|
||||
102,
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
username='user.name',
|
||||
password='password'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
the password is {{
|
||||
(secret['items']
|
||||
| items2dict(key_name='slug',
|
||||
value_name='itemValue'))['password']
|
||||
}}
|
||||
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.tss',
|
||||
102,
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
username='user.name',
|
||||
password='password',
|
||||
domain='domain'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
the password is {{
|
||||
(secret['items']
|
||||
| items2dict(key_name='slug',
|
||||
value_name='itemValue'))['password']
|
||||
}}
|
||||
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret_password: >-
|
||||
{{
|
||||
((lookup(
|
||||
'community.general.tss',
|
||||
102,
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
token='thycotic_access_token',
|
||||
) | from_json).get('items') | items2dict(key_name='slug', value_name='itemValue'))['password']
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: the password is {{ secret_password }}
|
||||
|
||||
# Private key stores into certificate file which is attached with secret.
|
||||
# If fetch_attachments=True then private key file will be download on specified path
|
||||
# and file content will display in debug message.
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.tss',
|
||||
102,
|
||||
fetch_attachments=True,
|
||||
file_download_path='/home/certs',
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
token='thycotic_access_token'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
the private key is {{
|
||||
(secret['items']
|
||||
| items2dict(key_name='slug',
|
||||
value_name='itemValue'))['private-key']
|
||||
}}
|
||||
|
||||
# If fetch_secret_ids_from_folder=true then secret IDs are in a folder is fetched based on folder ID
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.tss',
|
||||
102,
|
||||
fetch_secret_ids_from_folder=true,
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
token='thycotic_access_token'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
the secret id's are {{
|
||||
secret
|
||||
}}
|
||||
|
||||
# If secret ID is 0 and secret_path has value then secret is fetched by secret path
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.tss',
|
||||
0,
|
||||
secret_path='\folderName\secretName'
|
||||
base_url='https://secretserver.domain.com/SecretServer/',
|
||||
username='user.name',
|
||||
password='password'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
the password is {{
|
||||
(secret['items']
|
||||
| items2dict(key_name='slug',
|
||||
value_name='itemValue'))['password']
|
||||
}}
|
||||
"""
|
||||
|
||||
import abc
|
||||
import os
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.module_utils import six
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
try:
|
||||
from delinea.secrets.server import SecretServer, SecretServerError, PasswordGrantAuthorizer, DomainPasswordGrantAuthorizer, AccessTokenAuthorizer
|
||||
|
||||
HAS_TSS_SDK = True
|
||||
HAS_DELINEA_SS_SDK = True
|
||||
HAS_TSS_AUTHORIZER = True
|
||||
except ImportError:
|
||||
try:
|
||||
from thycotic.secrets.server import SecretServer, SecretServerError, PasswordGrantAuthorizer, DomainPasswordGrantAuthorizer, AccessTokenAuthorizer
|
||||
|
||||
HAS_TSS_SDK = True
|
||||
HAS_DELINEA_SS_SDK = False
|
||||
HAS_TSS_AUTHORIZER = True
|
||||
except ImportError:
|
||||
SecretServer = None
|
||||
SecretServerError = None
|
||||
HAS_TSS_SDK = False
|
||||
HAS_DELINEA_SS_SDK = False
|
||||
PasswordGrantAuthorizer = None
|
||||
DomainPasswordGrantAuthorizer = None
|
||||
AccessTokenAuthorizer = None
|
||||
HAS_TSS_AUTHORIZER = False
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class TSSClient(object):
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
|
||||
@staticmethod
|
||||
def from_params(**server_parameters):
|
||||
if HAS_TSS_AUTHORIZER:
|
||||
return TSSClientV1(**server_parameters)
|
||||
else:
|
||||
return TSSClientV0(**server_parameters)
|
||||
|
||||
def get_secret(self, term, secret_path, fetch_file_attachments, file_download_path):
|
||||
display.debug("tss_lookup term: %s" % term)
|
||||
secret_id = self._term_to_secret_id(term)
|
||||
if secret_id == 0 and secret_path:
|
||||
fetch_secret_by_path = True
|
||||
display.vvv(u"Secret Server lookup of Secret with path %s" % secret_path)
|
||||
else:
|
||||
fetch_secret_by_path = False
|
||||
display.vvv(u"Secret Server lookup of Secret with ID %d" % secret_id)
|
||||
|
||||
if fetch_file_attachments:
|
||||
if fetch_secret_by_path:
|
||||
obj = self._client.get_secret_by_path(secret_path, fetch_file_attachments)
|
||||
else:
|
||||
obj = self._client.get_secret(secret_id, fetch_file_attachments)
|
||||
for i in obj['items']:
|
||||
if file_download_path and os.path.isdir(file_download_path):
|
||||
if i['isFile']:
|
||||
try:
|
||||
file_content = i['itemValue'].content
|
||||
with open(os.path.join(file_download_path, str(obj['id']) + "_" + i['slug']), "wb") as f:
|
||||
f.write(file_content)
|
||||
except ValueError:
|
||||
raise AnsibleOptionsError("Failed to download {0}".format(str(i['slug'])))
|
||||
except AttributeError:
|
||||
display.warning("Could not read file content for {0}".format(str(i['slug'])))
|
||||
finally:
|
||||
i['itemValue'] = "*** Not Valid For Display ***"
|
||||
else:
|
||||
raise AnsibleOptionsError("File download path does not exist")
|
||||
return obj
|
||||
else:
|
||||
if fetch_secret_by_path:
|
||||
return self._client.get_secret_by_path(secret_path, False)
|
||||
else:
|
||||
return self._client.get_secret_json(secret_id)
|
||||
|
||||
def get_secret_ids_by_folderid(self, term):
|
||||
display.debug("tss_lookup term: %s" % term)
|
||||
folder_id = self._term_to_folder_id(term)
|
||||
display.vvv(u"Secret Server lookup of Secret id's with Folder ID %d" % folder_id)
|
||||
|
||||
return self._client.get_secret_ids_by_folderid(folder_id)
|
||||
|
||||
@staticmethod
|
||||
def _term_to_secret_id(term):
|
||||
try:
|
||||
return int(term)
|
||||
except ValueError:
|
||||
raise AnsibleOptionsError("Secret ID must be an integer")
|
||||
|
||||
@staticmethod
|
||||
def _term_to_folder_id(term):
|
||||
try:
|
||||
return int(term)
|
||||
except ValueError:
|
||||
raise AnsibleOptionsError("Folder ID must be an integer")
|
||||
|
||||
|
||||
class TSSClientV0(TSSClient):
|
||||
def __init__(self, **server_parameters):
|
||||
super(TSSClientV0, self).__init__()
|
||||
|
||||
if server_parameters.get("domain"):
|
||||
raise AnsibleError("The 'domain' option requires 'python-tss-sdk' version 1.0.0 or greater")
|
||||
|
||||
self._client = SecretServer(
|
||||
server_parameters["base_url"],
|
||||
server_parameters["username"],
|
||||
server_parameters["password"],
|
||||
server_parameters["api_path_uri"],
|
||||
server_parameters["token_path_uri"],
|
||||
)
|
||||
|
||||
|
||||
class TSSClientV1(TSSClient):
|
||||
def __init__(self, **server_parameters):
|
||||
super(TSSClientV1, self).__init__()
|
||||
|
||||
authorizer = self._get_authorizer(**server_parameters)
|
||||
self._client = SecretServer(
|
||||
server_parameters["base_url"], authorizer, server_parameters["api_path_uri"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_authorizer(**server_parameters):
|
||||
if server_parameters.get("token"):
|
||||
return AccessTokenAuthorizer(
|
||||
server_parameters["token"],
|
||||
)
|
||||
|
||||
if server_parameters.get("domain"):
|
||||
return DomainPasswordGrantAuthorizer(
|
||||
server_parameters["base_url"],
|
||||
server_parameters["username"],
|
||||
server_parameters["domain"],
|
||||
server_parameters["password"],
|
||||
server_parameters["token_path_uri"],
|
||||
)
|
||||
|
||||
return PasswordGrantAuthorizer(
|
||||
server_parameters["base_url"],
|
||||
server_parameters["username"],
|
||||
server_parameters["password"],
|
||||
server_parameters["token_path_uri"],
|
||||
)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables, **kwargs):
|
||||
if not HAS_TSS_SDK:
|
||||
raise AnsibleError("python-tss-sdk must be installed to use this plugin")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
tss = TSSClient.from_params(
|
||||
base_url=self.get_option("base_url"),
|
||||
username=self.get_option("username"),
|
||||
password=self.get_option("password"),
|
||||
domain=self.get_option("domain"),
|
||||
token=self.get_option("token"),
|
||||
api_path_uri=self.get_option("api_path_uri"),
|
||||
token_path_uri=self.get_option("token_path_uri"),
|
||||
)
|
||||
|
||||
try:
|
||||
if self.get_option("fetch_secret_ids_from_folder"):
|
||||
if HAS_DELINEA_SS_SDK:
|
||||
return [tss.get_secret_ids_by_folderid(term) for term in terms]
|
||||
else:
|
||||
raise AnsibleError("latest python-tss-sdk must be installed to use this plugin")
|
||||
else:
|
||||
return [
|
||||
tss.get_secret(
|
||||
term,
|
||||
self.get_option("secret_path"),
|
||||
self.get_option("fetch_attachments"),
|
||||
self.get_option("file_download_path"),
|
||||
)
|
||||
for term in terms
|
||||
]
|
||||
except SecretServerError as error:
|
||||
raise AnsibleError("Secret Server lookup failure: %s" % error.message)
|
Reference in New Issue
Block a user