All lookups ported to config system (#74108)
* all lookups to support config system - added get_options to get full dict with all opts - fixed tests to match new error messages - kept inline string k=v parsing methods for backwards compat - placeholder depredation for inline string k=v parsing - updated tests and examples to also show new way - refactored and added comments to most custom k=v parsing - added missing docs for template_vars to template - normalized error messages and exception types - fixed constants default - better details value errors Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
4819e9301b
commit
84e473a26e
18 changed files with 270 additions and 149 deletions
|
@ -5,20 +5,23 @@
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
from ansible.config.manager import ConfigManager, ensure_type, get_ini_config_value
|
from ansible.config.manager import ConfigManager, ensure_type
|
||||||
from ansible.module_utils._text import to_text
|
from ansible.module_utils._text import to_text
|
||||||
from ansible.module_utils.common.collections import Sequence
|
from ansible.module_utils.common.collections import Sequence
|
||||||
from ansible.module_utils.parsing.convert_bool import boolean, BOOLEANS_TRUE
|
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
|
||||||
from ansible.module_utils.six import string_types
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.release import __version__
|
||||||
from ansible.utils.fqcn import add_internal_fqcns
|
from ansible.utils.fqcn import add_internal_fqcns
|
||||||
|
|
||||||
|
# 4 versions above current
|
||||||
|
default_deprecated = to_text(float('.'.join(__version__.split('.')[0:2])) + 0.04)
|
||||||
|
|
||||||
|
|
||||||
def _warning(msg):
|
def _warning(msg):
|
||||||
''' display is not guaranteed here, nor it being the full class, but try anyways, fallback to sys.stderr.write '''
|
''' display is not guaranteed here, nor it being the full class, but try anyways, fallback to sys.stderr.write '''
|
||||||
|
@ -30,7 +33,7 @@ def _warning(msg):
|
||||||
sys.stderr.write(' [WARNING] %s\n' % (msg))
|
sys.stderr.write(' [WARNING] %s\n' % (msg))
|
||||||
|
|
||||||
|
|
||||||
def _deprecated(msg, version='2.8'):
|
def _deprecated(msg, version=default_deprecated):
|
||||||
''' display is not guaranteed here, nor it being the full class, but try anyways, fallback to sys.stderr.write '''
|
''' display is not guaranteed here, nor it being the full class, but try anyways, fallback to sys.stderr.write '''
|
||||||
try:
|
try:
|
||||||
from ansible.utils.display import Display
|
from ansible.utils.display import Display
|
||||||
|
|
|
@ -61,6 +61,13 @@ class AnsiblePlugin(with_metaclass(ABCMeta, object)):
|
||||||
self.set_option(option, option_value)
|
self.set_option(option, option_value)
|
||||||
return self._options.get(option)
|
return self._options.get(option)
|
||||||
|
|
||||||
|
def get_options(self, hostvars=None):
|
||||||
|
options = {}
|
||||||
|
defs = C.config.get_configuration_definitions(plugin_type=get_plugin_class(self), name=self._load_name)
|
||||||
|
for option in defs:
|
||||||
|
options[option] = self.get_option(option, hostvars=hostvars)
|
||||||
|
return options
|
||||||
|
|
||||||
def set_option(self, option, value):
|
def set_option(self, option, value):
|
||||||
self._options[option] = value
|
self._options[option] = value
|
||||||
|
|
||||||
|
|
|
@ -123,3 +123,8 @@ class LookupBase(AnsiblePlugin):
|
||||||
self._display.warning("Unable to find '%s' in expected paths (use -vvvvv to see paths)" % needle)
|
self._display.warning("Unable to find '%s' in expected paths (use -vvvvv to see paths)" % needle)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _deprecate_inline_kv(self):
|
||||||
|
# TODO: place holder to deprecate in future version allowing for long transition period
|
||||||
|
# self._display.deprecated('Passing inline k=v values embeded in a string to this lookup. Use direct ,k=v, k2=v2 syntax instead.', version='2.18')
|
||||||
|
pass
|
||||||
|
|
|
@ -132,6 +132,7 @@ class LookupModule(LookupBase):
|
||||||
raise AnsibleOptionsError('"on_missing" must be a string and one of "error", "warn" or "skip", not %s' % missing)
|
raise AnsibleOptionsError('"on_missing" must be a string and one of "error", "warn" or "skip", not %s' % missing)
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
for term in terms:
|
for term in terms:
|
||||||
if not isinstance(term, string_types):
|
if not isinstance(term, string_types):
|
||||||
raise AnsibleOptionsError('Invalid setting identifier, "%s" is not a string, its a %s' % (term, type(term)))
|
raise AnsibleOptionsError('Invalid setting identifier, "%s" is not a string, its a %s' % (term, type(term)))
|
||||||
|
|
|
@ -19,7 +19,6 @@ DOCUMENTATION = """
|
||||||
default: "1"
|
default: "1"
|
||||||
default:
|
default:
|
||||||
description: what to return if the value is not found in the file.
|
description: what to return if the value is not found in the file.
|
||||||
default: ''
|
|
||||||
delimiter:
|
delimiter:
|
||||||
description: field separator in the file, for a tab you can specify C(TAB) or C(\\t).
|
description: field separator in the file, for a tab you can specify C(TAB) or C(\\t).
|
||||||
default: TAB
|
default: TAB
|
||||||
|
@ -40,20 +39,22 @@ DOCUMENTATION = """
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
- name: Match 'Li' on the first column, return the second column (0 based index)
|
- name: Match 'Li' on the first column, return the second column (0 based index)
|
||||||
debug: msg="The atomic number of Lithium is {{ lookup('csvfile', 'Li file=elements.csv delimiter=,') }}"
|
debug: msg="The atomic number of Lithium is {{ lookup('csvfile', 'Li', file='elements.csv', delimiter=',') }}"
|
||||||
|
|
||||||
- name: msg="Match 'Li' on the first column, but return the 3rd column (columns start counting after the match)"
|
- name: msg="Match 'Li' on the first column, but return the 3rd column (columns start counting after the match)"
|
||||||
debug: msg="The atomic mass of Lithium is {{ lookup('csvfile', 'Li file=elements.csv delimiter=, col=2') }}"
|
debug: msg="The atomic mass of Lithium is {{ lookup('csvfile', 'Li', file='elements.csv', delimiter=',', col=2) }}"
|
||||||
|
|
||||||
- name: Define Values From CSV File
|
- name: Define Values From CSV File, this reads file in one go, but you could also use col= to read each in it's own lookup.
|
||||||
set_fact:
|
set_fact:
|
||||||
loop_ip: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=1') }}"
|
loop_ip: "{{ csvline[0] }}"
|
||||||
int_ip: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=2') }}"
|
int_ip: "{{ csvline[1] }}"
|
||||||
int_mask: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=3') }}"
|
int_mask: "{{ csvline[2] }}"
|
||||||
int_name: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=4') }}"
|
int_name: "{{ csvline[3] }}"
|
||||||
local_as: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=5') }}"
|
local_as: "{{ csvline[4] }}"
|
||||||
neighbor_as: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=6') }}"
|
neighbor_as: "{{ csvline[5] }}"
|
||||||
neigh_int_ip: "{{ lookup('csvfile', bgp_neighbor_ip +' file=bgp_neighbors.csv delimiter=, col=7') }}"
|
neigh_int_ip: "{{ csvline[6] }}"
|
||||||
|
vars:
|
||||||
|
csvline = "{{ lookup('csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',') }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ class LookupModule(LookupBase):
|
||||||
def read_csv(self, filename, key, delimiter, encoding='utf-8', dflt=None, col=1):
|
def read_csv(self, filename, key, delimiter, encoding='utf-8', dflt=None, col=1):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = open(filename, 'rb')
|
f = open(to_bytes(filename), 'rb')
|
||||||
creader = CSVReader(f, delimiter=to_native(delimiter), encoding=encoding)
|
creader = CSVReader(f, delimiter=to_native(delimiter), encoding=encoding)
|
||||||
|
|
||||||
for row in creader:
|
for row in creader:
|
||||||
|
@ -136,6 +137,11 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
|
||||||
|
# populate options
|
||||||
|
paramvals = self.get_options()
|
||||||
|
|
||||||
for term in terms:
|
for term in terms:
|
||||||
kv = parse_kv(term)
|
kv = parse_kv(term)
|
||||||
|
|
||||||
|
@ -144,25 +150,21 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
key = kv['_raw_params']
|
key = kv['_raw_params']
|
||||||
|
|
||||||
paramvals = {
|
# parameters override per term using k/v
|
||||||
'col': "1", # column to return
|
|
||||||
'default': None,
|
|
||||||
'delimiter': "TAB",
|
|
||||||
'file': 'ansible.csv',
|
|
||||||
'encoding': 'utf-8',
|
|
||||||
}
|
|
||||||
|
|
||||||
# parameters specified?
|
|
||||||
try:
|
try:
|
||||||
for name, value in kv.items():
|
for name, value in kv.items():
|
||||||
if name == '_raw_params':
|
if name == '_raw_params':
|
||||||
continue
|
continue
|
||||||
if name not in paramvals:
|
if name not in paramvals:
|
||||||
raise AnsibleAssertionError('%s not in paramvals' % name)
|
raise AnsibleAssertionError('%s is not a valid option' % name)
|
||||||
|
|
||||||
|
self._deprecate_inline_kv()
|
||||||
paramvals[name] = value
|
paramvals[name] = value
|
||||||
|
|
||||||
except (ValueError, AssertionError) as e:
|
except (ValueError, AssertionError) as e:
|
||||||
raise AnsibleError(e)
|
raise AnsibleError(e)
|
||||||
|
|
||||||
|
# default is just placeholder for real tab
|
||||||
if paramvals['delimiter'] == 'TAB':
|
if paramvals['delimiter'] == 'TAB':
|
||||||
paramvals['delimiter'] = "\t"
|
paramvals['delimiter'] = "\t"
|
||||||
|
|
||||||
|
@ -174,4 +176,5 @@ class LookupModule(LookupBase):
|
||||||
ret.append(v)
|
ret.append(v)
|
||||||
else:
|
else:
|
||||||
ret.append(var)
|
ret.append(var)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -62,7 +62,7 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
def run(self, terms, variables=None, **kwargs):
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
|
||||||
# FIXME: can remove once with_ special case is removed
|
# NOTE: can remove if with_ is removed
|
||||||
if not isinstance(terms, list):
|
if not isinstance(terms, list):
|
||||||
terms = [terms]
|
terms = [terms]
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ class LookupModule(LookupBase):
|
||||||
def run(self, terms, variables=None, **kwargs):
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
|
||||||
for term in terms:
|
for term in terms:
|
||||||
display.debug("File lookup term: %s" % term)
|
display.debug("File lookup term: %s" % term)
|
||||||
|
@ -73,9 +74,9 @@ class LookupModule(LookupBase):
|
||||||
if lookupfile:
|
if lookupfile:
|
||||||
b_contents, show_data = self._loader._get_file_contents(lookupfile)
|
b_contents, show_data = self._loader._get_file_contents(lookupfile)
|
||||||
contents = to_text(b_contents, errors='surrogate_or_strict')
|
contents = to_text(b_contents, errors='surrogate_or_strict')
|
||||||
if kwargs.get('lstrip', False):
|
if self.get_option('lstrip'):
|
||||||
contents = contents.lstrip()
|
contents = contents.lstrip()
|
||||||
if kwargs.get('rstrip', True):
|
if self.get_option('rstrip'):
|
||||||
contents = contents.rstrip()
|
contents = contents.rstrip()
|
||||||
ret.append(contents)
|
ret.append(contents)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -23,8 +23,12 @@ DOCUMENTATION = """
|
||||||
description: list of file names
|
description: list of file names
|
||||||
files:
|
files:
|
||||||
description: list of file names
|
description: list of file names
|
||||||
|
type: list
|
||||||
|
default: []
|
||||||
paths:
|
paths:
|
||||||
description: list of paths in which to look for the files
|
description: list of paths in which to look for the files
|
||||||
|
type: list
|
||||||
|
default: []
|
||||||
skip:
|
skip:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: False
|
default: False
|
||||||
|
@ -106,71 +110,100 @@ import os
|
||||||
|
|
||||||
from jinja2.exceptions import UndefinedError
|
from jinja2.exceptions import UndefinedError
|
||||||
|
|
||||||
from ansible.errors import AnsibleFileNotFound, AnsibleLookupError, AnsibleUndefinedVariable
|
from ansible.errors import AnsibleLookupError, AnsibleUndefinedVariable
|
||||||
|
from ansible.module_utils.common._collections_compat import Mapping, Sequence
|
||||||
from ansible.module_utils.six import string_types
|
from ansible.module_utils.six import string_types
|
||||||
from ansible.module_utils.parsing.convert_bool import boolean
|
|
||||||
from ansible.plugins.lookup import LookupBase
|
from ansible.plugins.lookup import LookupBase
|
||||||
|
|
||||||
|
|
||||||
|
def _split_on(terms, spliters=','):
|
||||||
|
|
||||||
|
# TODO: fix as it does not allow spaces in names
|
||||||
|
termlist = []
|
||||||
|
if isinstance(terms, string_types):
|
||||||
|
for spliter in spliters:
|
||||||
|
terms = terms.replace(spliter, ' ')
|
||||||
|
termlist = terms.split(' ')
|
||||||
|
else:
|
||||||
|
# added since options will already listify
|
||||||
|
for t in terms:
|
||||||
|
termlist.extend(_split_on(t, spliters))
|
||||||
|
|
||||||
|
return termlist
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
|
|
||||||
def run(self, terms, variables, **kwargs):
|
def _process_terms(self, terms, variables, kwargs):
|
||||||
|
|
||||||
anydict = False
|
|
||||||
skip = False
|
|
||||||
|
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, dict):
|
|
||||||
anydict = True
|
|
||||||
|
|
||||||
total_search = []
|
total_search = []
|
||||||
if anydict:
|
skip = False
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, dict):
|
|
||||||
|
|
||||||
files = term.get('files', [])
|
# can use a dict instead of list item to pass inline config
|
||||||
paths = term.get('paths', [])
|
for term in terms:
|
||||||
skip = boolean(term.get('skip', False), strict=False)
|
if isinstance(term, Mapping):
|
||||||
|
self.set_options(var_options=variables, direct=term)
|
||||||
|
elif isinstance(term, string_types):
|
||||||
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
elif isinstance(term, Sequence):
|
||||||
|
partial, skip = self._process_terms(term, variables, kwargs)
|
||||||
|
total_search.extend(partial)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise AnsibleLookupError("Invalid term supplied, can handle string, mapping or list of strings but got: %s for %s" % (type(term), term))
|
||||||
|
|
||||||
filelist = files
|
files = self.get_option('files')
|
||||||
if isinstance(files, string_types):
|
paths = self.get_option('paths')
|
||||||
files = files.replace(',', ' ')
|
|
||||||
files = files.replace(';', ' ')
|
|
||||||
filelist = files.split(' ')
|
|
||||||
|
|
||||||
pathlist = paths
|
# NOTE: this is used as 'global' but can be set many times?!?!?
|
||||||
if paths:
|
skip = self.get_option('skip')
|
||||||
if isinstance(paths, string_types):
|
|
||||||
paths = paths.replace(',', ' ')
|
|
||||||
paths = paths.replace(':', ' ')
|
|
||||||
paths = paths.replace(';', ' ')
|
|
||||||
pathlist = paths.split(' ')
|
|
||||||
|
|
||||||
if not pathlist:
|
# magic extra spliting to create lists
|
||||||
total_search = filelist
|
filelist = _split_on(files, ',;')
|
||||||
else:
|
pathlist = _split_on(paths, ',:;')
|
||||||
for path in pathlist:
|
|
||||||
for fn in filelist:
|
|
||||||
f = os.path.join(path, fn)
|
|
||||||
total_search.append(f)
|
|
||||||
else:
|
|
||||||
total_search.append(term)
|
|
||||||
else:
|
|
||||||
total_search = self._flatten(terms)
|
|
||||||
|
|
||||||
|
# create search structure
|
||||||
|
if pathlist:
|
||||||
|
for path in pathlist:
|
||||||
|
for fn in filelist:
|
||||||
|
f = os.path.join(path, fn)
|
||||||
|
total_search.append(f)
|
||||||
|
elif filelist:
|
||||||
|
# NOTE: this seems wrong, should be 'extend' as any option/entry can clobber all
|
||||||
|
total_search = filelist
|
||||||
|
else:
|
||||||
|
total_search.append(term)
|
||||||
|
|
||||||
|
return total_search, skip
|
||||||
|
|
||||||
|
def run(self, terms, variables, **kwargs):
|
||||||
|
|
||||||
|
total_search, skip = self._process_terms(terms, variables, kwargs)
|
||||||
|
|
||||||
|
# NOTE: during refactor noticed that the 'using a dict' as term
|
||||||
|
# is designed to only work with 'one' otherwise inconsistencies will appear.
|
||||||
|
# see other notes below.
|
||||||
|
|
||||||
|
# actually search
|
||||||
|
subdir = getattr(self, '_subdir', 'files')
|
||||||
|
|
||||||
|
path = None
|
||||||
for fn in total_search:
|
for fn in total_search:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fn = self._templar.template(fn)
|
fn = self._templar.template(fn)
|
||||||
except (AnsibleUndefinedVariable, UndefinedError):
|
except (AnsibleUndefinedVariable, UndefinedError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# get subdir if set by task executor, default to files otherwise
|
# get subdir if set by task executor, default to files otherwise
|
||||||
subdir = getattr(self, '_subdir', 'files')
|
|
||||||
path = None
|
|
||||||
path = self.find_file_in_search_path(variables, subdir, fn, ignore_missing=True)
|
path = self.find_file_in_search_path(variables, subdir, fn, ignore_missing=True)
|
||||||
|
|
||||||
|
# exit if we find one!
|
||||||
if path is not None:
|
if path is not None:
|
||||||
return [path]
|
return [path]
|
||||||
|
|
||||||
|
# if we get here, no file was found
|
||||||
if skip:
|
if skip:
|
||||||
|
# NOTE: global skip wont matter, only last 'skip' value in dict term
|
||||||
return []
|
return []
|
||||||
raise AnsibleLookupError("No file was found when using first_found. Use errors='ignore' to allow this task to be skipped if no "
|
raise AnsibleLookupError("No file was found when using first_found. Use errors='ignore' to allow this task to be skipped if no files are found")
|
||||||
"files are found")
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ DOCUMENTATION = """
|
||||||
choices: ['ini', 'properties']
|
choices: ['ini', 'properties']
|
||||||
file:
|
file:
|
||||||
description: Name of the file to load.
|
description: Name of the file to load.
|
||||||
default: ansible.ini
|
default: 'ansible.ini'
|
||||||
section:
|
section:
|
||||||
default: global
|
default: global
|
||||||
description: Section where to lookup the key.
|
description: Section where to lookup the key.
|
||||||
|
@ -40,16 +40,15 @@ DOCUMENTATION = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
- debug: msg="User in integration is {{ lookup('ini', 'user section=integration file=users.ini') }}"
|
- debug: msg="User in integration is {{ lookup('ini', 'user', section='integration', file='users.ini') }}"
|
||||||
|
|
||||||
- debug: msg="User in production is {{ lookup('ini', 'user section=production file=users.ini') }}"
|
- debug: msg="User in production is {{ lookup('ini', 'user', section='production', file='users.ini') }}"
|
||||||
|
|
||||||
- debug: msg="user.name is {{ lookup('ini', 'user.name type=properties file=user.properties') }}"
|
- debug: msg="user.name is {{ lookup('ini', 'user.name', type='properties', file='user.properties') }}"
|
||||||
|
|
||||||
- debug:
|
- debug:
|
||||||
msg: "{{ item }}"
|
msg: "{{ item }}"
|
||||||
with_ini:
|
loop: "{{q('ini', '.*', section='section1', file='test.ini', re=True)}}"
|
||||||
- '.* section=section1 file=test.ini re=True'
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN = """
|
RETURN = """
|
||||||
|
@ -59,37 +58,48 @@ _raw:
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
from io import StringIO
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleLookupError
|
||||||
from ansible.module_utils.six.moves import configparser
|
from ansible.module_utils.six.moves import configparser
|
||||||
from ansible.module_utils._text import to_bytes, to_text
|
from ansible.module_utils._text import to_bytes, to_text, to_native
|
||||||
from ansible.module_utils.common._collections_compat import MutableSequence
|
from ansible.module_utils.common._collections_compat import MutableSequence
|
||||||
from ansible.plugins.lookup import LookupBase
|
from ansible.plugins.lookup import LookupBase
|
||||||
|
|
||||||
|
|
||||||
def _parse_params(term):
|
def _parse_params(term, paramvals):
|
||||||
'''Safely split parameter term to preserve spaces'''
|
'''Safely split parameter term to preserve spaces'''
|
||||||
|
|
||||||
keys = ['key', 'type', 'section', 'file', 're', 'default', 'encoding']
|
# TODO: deprecate this method
|
||||||
params = {}
|
valid_keys = paramvals.keys()
|
||||||
for k in keys:
|
params = defaultdict(lambda: '')
|
||||||
params[k] = ''
|
|
||||||
|
|
||||||
thiskey = 'key'
|
# TODO: check kv_parser to see if it can handle spaces this same way
|
||||||
|
keys = []
|
||||||
|
thiskey = 'key' # initialize for 'lookup item'
|
||||||
for idp, phrase in enumerate(term.split()):
|
for idp, phrase in enumerate(term.split()):
|
||||||
for k in keys:
|
|
||||||
if ('%s=' % k) in phrase:
|
# update current key if used
|
||||||
thiskey = k
|
if '=' in phrase:
|
||||||
|
for k in valid_keys:
|
||||||
|
if ('%s=' % k) in phrase:
|
||||||
|
thiskey = k
|
||||||
|
|
||||||
|
# if first term or key does not exist
|
||||||
if idp == 0 or not params[thiskey]:
|
if idp == 0 or not params[thiskey]:
|
||||||
params[thiskey] = phrase
|
params[thiskey] = phrase
|
||||||
|
keys.append(thiskey)
|
||||||
else:
|
else:
|
||||||
|
# append to existing key
|
||||||
params[thiskey] += ' ' + phrase
|
params[thiskey] += ' ' + phrase
|
||||||
|
|
||||||
rparams = [params[x] for x in keys if params[x]]
|
# return list of values
|
||||||
return rparams
|
return [params[x] for x in keys]
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
|
@ -108,36 +118,36 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
def run(self, terms, variables=None, **kwargs):
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
|
||||||
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
paramvals = self.get_options()
|
||||||
|
|
||||||
self.cp = configparser.ConfigParser()
|
self.cp = configparser.ConfigParser()
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for term in terms:
|
for term in terms:
|
||||||
params = _parse_params(term)
|
|
||||||
key = params[0]
|
|
||||||
|
|
||||||
paramvals = {
|
|
||||||
'file': 'ansible.ini',
|
|
||||||
're': False,
|
|
||||||
'default': None,
|
|
||||||
'section': "global",
|
|
||||||
'type': "ini",
|
|
||||||
'encoding': 'utf-8',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
key = term
|
||||||
# parameters specified?
|
# parameters specified?
|
||||||
try:
|
if '=' in term or ' ' in term.strip():
|
||||||
for param in params[1:]:
|
self._deprecate_inline_kv()
|
||||||
name, value = param.split('=')
|
params = _parse_params(term, paramvals)
|
||||||
if name not in paramvals:
|
try:
|
||||||
raise AnsibleAssertionError('%s not in paramvals' %
|
for param in params:
|
||||||
name)
|
if '=' in param:
|
||||||
paramvals[name] = value
|
name, value = param.split('=')
|
||||||
except (ValueError, AssertionError) as e:
|
if name not in paramvals:
|
||||||
raise AnsibleError(e)
|
raise AnsibleLookupError('%s is not a valid option.' % name)
|
||||||
|
paramvals[name] = value
|
||||||
|
elif key == term:
|
||||||
|
# only take first, this format never supported multiple keys inline
|
||||||
|
key = param
|
||||||
|
except ValueError as e:
|
||||||
|
# bad params passed
|
||||||
|
raise AnsibleLookupError("Could not use '%s' from '%s': %s" % (param, params, to_native(e)), orig_exc=e)
|
||||||
|
|
||||||
|
# TODO: look to use cache to avoid redoing this for every term if they use same file
|
||||||
# Retrieve file path
|
# Retrieve file path
|
||||||
path = self.find_file_in_search_path(variables, 'files',
|
path = self.find_file_in_search_path(variables, 'files', paramvals['file'])
|
||||||
paramvals['file'])
|
|
||||||
|
|
||||||
# Create StringIO later used to parse ini
|
# Create StringIO later used to parse ini
|
||||||
config = StringIO()
|
config = StringIO()
|
||||||
|
@ -148,14 +158,12 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
# Open file using encoding
|
# Open file using encoding
|
||||||
contents, show_data = self._loader._get_file_contents(path)
|
contents, show_data = self._loader._get_file_contents(path)
|
||||||
contents = to_text(contents, errors='surrogate_or_strict',
|
contents = to_text(contents, errors='surrogate_or_strict', encoding=paramvals['encoding'])
|
||||||
encoding=paramvals['encoding'])
|
|
||||||
config.write(contents)
|
config.write(contents)
|
||||||
config.seek(0, os.SEEK_SET)
|
config.seek(0, os.SEEK_SET)
|
||||||
|
|
||||||
self.cp.readfp(config)
|
self.cp.readfp(config)
|
||||||
var = self.get_value(key, paramvals['section'],
|
var = self.get_value(key, paramvals['section'], paramvals['default'], paramvals['re'])
|
||||||
paramvals['default'], paramvals['re'])
|
|
||||||
if var is not None:
|
if var is not None:
|
||||||
if isinstance(var, MutableSequence):
|
if isinstance(var, MutableSequence):
|
||||||
for v in var:
|
for v in var:
|
||||||
|
|
|
@ -40,6 +40,11 @@ DOCUMENTATION = """
|
||||||
default: False
|
default: False
|
||||||
version_added: '2.11'
|
version_added: '2.11'
|
||||||
type: bool
|
type: bool
|
||||||
|
template_vars:
|
||||||
|
description: A dictionary, the keys become additional variables available for templating.
|
||||||
|
default: {}
|
||||||
|
version_added: '2.3'
|
||||||
|
type: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
|
@ -78,13 +83,17 @@ display = Display()
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
|
|
||||||
def run(self, terms, variables, **kwargs):
|
def run(self, terms, variables, **kwargs):
|
||||||
convert_data_p = kwargs.get('convert_data', True)
|
|
||||||
lookup_template_vars = kwargs.get('template_vars', {})
|
|
||||||
jinja2_native = kwargs.get('jinja2_native', False)
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
variable_start_string = kwargs.get('variable_start_string', None)
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
variable_end_string = kwargs.get('variable_end_string', None)
|
|
||||||
|
# capture options
|
||||||
|
convert_data_p = self.get_option('convert_data')
|
||||||
|
lookup_template_vars = self.get_option('template_vars')
|
||||||
|
jinja2_native = self.get_option('jinja2_native')
|
||||||
|
variable_start_string = self.get_option('variable_start_string')
|
||||||
|
variable_end_string = self.get_option('variable_end_string')
|
||||||
|
|
||||||
if USE_JINJA2_NATIVE and not jinja2_native:
|
if USE_JINJA2_NATIVE and not jinja2_native:
|
||||||
templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment)
|
templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment)
|
||||||
|
|
|
@ -42,10 +42,10 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
def run(self, terms, variables=None, **kwargs):
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
|
||||||
self.set_options(direct=kwargs)
|
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
|
||||||
for term in terms:
|
for term in terms:
|
||||||
display.debug("Unvault lookup term: %s" % term)
|
display.debug("Unvault lookup term: %s" % term)
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,7 @@ class LookupModule(LookupBase):
|
||||||
if variables is None:
|
if variables is None:
|
||||||
raise AnsibleError('No variables available to search')
|
raise AnsibleError('No variables available to search')
|
||||||
|
|
||||||
# no options, yet
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
# self.set_options(direct=kwargs)
|
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
variable_names = list(variables.keys())
|
variable_names = list(variables.keys())
|
||||||
|
|
|
@ -79,7 +79,7 @@ class LookupModule(LookupBase):
|
||||||
self._templar.available_variables = variables
|
self._templar.available_variables = variables
|
||||||
myvars = getattr(self._templar, '_available_variables', {})
|
myvars = getattr(self._templar, '_available_variables', {})
|
||||||
|
|
||||||
self.set_options(direct=kwargs)
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
default = self.get_option('default')
|
default = self.get_option('default')
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
|
@ -42,7 +42,15 @@ from jinja2.loaders import FileSystemLoader
|
||||||
from jinja2.runtime import Context, StrictUndefined
|
from jinja2.runtime import Context, StrictUndefined
|
||||||
|
|
||||||
from ansible import constants as C
|
from ansible import constants as C
|
||||||
from ansible.errors import AnsibleError, AnsibleFilterError, AnsiblePluginRemovedError, AnsibleUndefinedVariable, AnsibleAssertionError
|
from ansible.errors import (
|
||||||
|
AnsibleAssertionError,
|
||||||
|
AnsibleError,
|
||||||
|
AnsibleFilterError,
|
||||||
|
AnsibleLookupError,
|
||||||
|
AnsibleOptionsError,
|
||||||
|
AnsiblePluginRemovedError,
|
||||||
|
AnsibleUndefinedVariable,
|
||||||
|
)
|
||||||
from ansible.module_utils.six import iteritems, string_types, text_type
|
from ansible.module_utils.six import iteritems, string_types, text_type
|
||||||
from ansible.module_utils.six.moves import range
|
from ansible.module_utils.six.moves import range
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||||
|
@ -997,7 +1005,22 @@ class Templar:
|
||||||
ran = instance.run(loop_terms, variables=self._available_variables, **kwargs)
|
ran = instance.run(loop_terms, variables=self._available_variables, **kwargs)
|
||||||
except (AnsibleUndefinedVariable, UndefinedError) as e:
|
except (AnsibleUndefinedVariable, UndefinedError) as e:
|
||||||
raise AnsibleUndefinedVariable(e)
|
raise AnsibleUndefinedVariable(e)
|
||||||
|
except AnsibleOptionsError as e:
|
||||||
|
# invalid options given to lookup, just reraise
|
||||||
|
raise e
|
||||||
|
except AnsibleLookupError as e:
|
||||||
|
# lookup handled error but still decided to bail
|
||||||
|
if self._fail_on_lookup_errors:
|
||||||
|
msg = 'Lookup failed but the error is being ignored: %s' % to_native(e)
|
||||||
|
if errors == 'warn':
|
||||||
|
display.warning(msg)
|
||||||
|
elif errors == 'ignore':
|
||||||
|
display.display(msg, log_only=True)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
return [] if wantlist else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# errors not handled by lookup
|
||||||
if self._fail_on_lookup_errors:
|
if self._fail_on_lookup_errors:
|
||||||
msg = u"An unhandled exception occurred while running the lookup plugin '%s'. Error was a %s, original message: %s" % \
|
msg = u"An unhandled exception occurred while running the lookup plugin '%s'. Error was a %s, original message: %s" % \
|
||||||
(name, type(e), to_text(e))
|
(name, type(e), to_text(e))
|
||||||
|
@ -1006,7 +1029,8 @@ class Templar:
|
||||||
elif errors == 'ignore':
|
elif errors == 'ignore':
|
||||||
display.display(msg, log_only=True)
|
display.display(msg, log_only=True)
|
||||||
else:
|
else:
|
||||||
raise AnsibleError(to_native(msg))
|
display.vvv('exception during Jinja2 execution: {0}'.format(format_exc()))
|
||||||
|
raise AnsibleError(to_native(msg), orig_exc=e)
|
||||||
return [] if wantlist else None
|
return [] if wantlist else None
|
||||||
|
|
||||||
if ran and allow_unsafe is False:
|
if ran and allow_unsafe is False:
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
- set_fact:
|
- name: using deprecated syntax but missing keyword
|
||||||
this_will_error: "{{ lookup('csvfile', 'file=people.csv delimiter=, col=1') }}"
|
set_fact:
|
||||||
|
this_will_error: "{{ lookup('csvfile', 'file=people.csv, delimiter=, col=1') }}"
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
register: no_keyword
|
register: no_keyword
|
||||||
|
|
||||||
- set_fact:
|
- name: extra arg in k=v syntax (deprecated)
|
||||||
|
set_fact:
|
||||||
this_will_error: "{{ lookup('csvfile', 'foo file=people.csv delimiter=, col=1 thisarg=doesnotexist') }}"
|
this_will_error: "{{ lookup('csvfile', 'foo file=people.csv delimiter=, col=1 thisarg=doesnotexist') }}"
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
register: invalid_arg
|
register: invalid_arg
|
||||||
|
|
||||||
|
- name: extra arg in config syntax
|
||||||
|
set_fact:
|
||||||
|
this_will_error: "{{ lookup('csvfile', 'foo', file='people.csv', delimiter=',' col=1, thisarg='doesnotexist') }}"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: invalid_arg2
|
||||||
|
|
||||||
- set_fact:
|
- set_fact:
|
||||||
this_will_error: "{{ lookup('csvfile', 'foo file=doesnotexist delimiter=, col=1') }}"
|
this_will_error: "{{ lookup('csvfile', 'foo', file='doesnotexist', delimiter=',', col=1) }}"
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
register: missing_file
|
register: missing_file
|
||||||
|
|
||||||
|
@ -19,24 +27,30 @@
|
||||||
- no_keyword is failed
|
- no_keyword is failed
|
||||||
- >
|
- >
|
||||||
"Search key is required but was not found" in no_keyword.msg
|
"Search key is required but was not found" in no_keyword.msg
|
||||||
|
- invalid_arg is failed
|
||||||
|
- invalid_arg2 is failed
|
||||||
- >
|
- >
|
||||||
"not in paramvals" in invalid_arg.msg
|
"is not a valid option" in invalid_arg.msg
|
||||||
- missing_file is failed
|
- missing_file is failed
|
||||||
- >
|
- >
|
||||||
"need string or buffer" in missing_file.msg or "expected str, bytes or os.PathLike object" in missing_file.msg
|
"need string or buffer" in missing_file.msg or
|
||||||
|
"expected str, bytes or os.PathLike object" in missing_file.msg or
|
||||||
|
"No such file or directory" in missing_file.msg
|
||||||
|
|
||||||
- name: Check basic comma-separated file
|
- name: Check basic comma-separated file
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- lookup('csvfile', 'Smith file=people.csv delimiter=, col=1') == "Jane"
|
- lookup('csvfile', 'Smith', file='people.csv', delimiter=',', col=1) == "Jane"
|
||||||
- lookup('csvfile', 'German von Lastname file=people.csv delimiter=, col=1') == "Demo"
|
- lookup('csvfile', 'German von Lastname file=people.csv delimiter=, col=1') == "Demo"
|
||||||
|
|
||||||
- name: Check tab-separated file
|
- name: Check tab-separated file
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- lookup('csvfile', 'electronics file=tabs.csv delimiter=TAB col=1') == "tvs"
|
- lookup('csvfile', 'electronics file=tabs.csv delimiter=TAB col=1') == "tvs"
|
||||||
- lookup('csvfile', 'fruit file=tabs.csv delimiter=TAB col=1') == "bananas"
|
- "lookup('csvfile', 'fruit', file='tabs.csv', delimiter='TAB', col=1) == 'bananas'"
|
||||||
- lookup('csvfile', 'fruit file=tabs.csv delimiter="\t" col=1') == "bananas"
|
- lookup('csvfile', 'fruit file=tabs.csv delimiter="\t" col=1') == "bananas"
|
||||||
|
- lookup('csvfile', 'electronics', 'fruit', file='tabs.csv', delimiter='\t', col=1) == "tvs,bananas"
|
||||||
|
- lookup('csvfile', 'electronics', 'fruit', file='tabs.csv', delimiter='\t', col=1, wantlist=True) == ["tvs", "bananas"]
|
||||||
|
|
||||||
- name: Check \x1a-separated file
|
- name: Check \x1a-separated file
|
||||||
assert:
|
assert:
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
- name: test with_first_found
|
- name: test with_first_found
|
||||||
#shell: echo {{ item }}
|
|
||||||
set_fact: "first_found={{ item }}"
|
set_fact: "first_found={{ item }}"
|
||||||
with_first_found:
|
with_first_found:
|
||||||
- "{{ role_path + '/files/does_not_exist' }}"
|
- "does_not_exist"
|
||||||
- "{{ role_path + '/files/foo1' }}"
|
- "foo1"
|
||||||
- "{{ role_path + '/files/bar1' }}"
|
- "{{ role_path + '/files/bar1' }}" # will only hit this if dwim search is broken
|
||||||
|
|
||||||
- name: set expected
|
- name: set expected
|
||||||
set_fact: first_expected="{{ role_path + '/files/foo1' }}"
|
set_fact: first_expected="{{ role_path + '/files/foo1' }}"
|
||||||
|
@ -24,6 +23,7 @@
|
||||||
vars:
|
vars:
|
||||||
params:
|
params:
|
||||||
files: "not_a_file.yaml"
|
files: "not_a_file.yaml"
|
||||||
|
skip: True
|
||||||
|
|
||||||
- name: verify q(first_found) result
|
- name: verify q(first_found) result
|
||||||
assert:
|
assert:
|
||||||
|
@ -71,3 +71,16 @@
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- "this_not_set is not defined"
|
- "this_not_set is not defined"
|
||||||
|
|
||||||
|
- name: test legacy formats
|
||||||
|
set_fact: hatethisformat={{item}}
|
||||||
|
vars:
|
||||||
|
params:
|
||||||
|
files: not/a/file.yaml;hosts
|
||||||
|
paths: not/a/path:/etc
|
||||||
|
loop: "{{ q('first_found', params) }}"
|
||||||
|
|
||||||
|
- name: verify /etc/hosts was found
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "hatethisformat == '/etc/hosts'"
|
||||||
|
|
|
@ -6,18 +6,18 @@
|
||||||
- name: "read properties value"
|
- name: "read properties value"
|
||||||
set_fact:
|
set_fact:
|
||||||
test1: "{{lookup('ini', 'value1 type=properties file=lookup.properties')}}"
|
test1: "{{lookup('ini', 'value1 type=properties file=lookup.properties')}}"
|
||||||
test2: "{{lookup('ini', 'value2 type=properties file=lookup.properties')}}"
|
test2: "{{lookup('ini', 'value2', type='properties', file='lookup.properties')}}"
|
||||||
test_dot: "{{lookup('ini', 'value.dot type=properties file=lookup.properties')}}"
|
test_dot: "{{lookup('ini', 'value.dot', type='properties', file='lookup.properties')}}"
|
||||||
field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}"
|
field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}"
|
||||||
- assert:
|
- assert:
|
||||||
that: "{{item}} is defined"
|
that: "{{item}} is defined"
|
||||||
with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
|
with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
|
||||||
- name: "read ini value"
|
- name: "read ini value"
|
||||||
set_fact:
|
set_fact:
|
||||||
value1_global: "{{lookup('ini', 'value1 section=global file=lookup.ini')}}"
|
value1_global: "{{lookup('ini', 'value1', section='global', file='lookup.ini')}}"
|
||||||
value2_global: "{{lookup('ini', 'value2 section=global file=lookup.ini')}}"
|
value2_global: "{{lookup('ini', 'value2', section='global', file='lookup.ini')}}"
|
||||||
value1_section1: "{{lookup('ini', 'value1 section=section1 file=lookup.ini')}}"
|
value1_section1: "{{lookup('ini', 'value1', section='section1', file='lookup.ini')}}"
|
||||||
field_with_unicode: "{{lookup('ini', 'unicode section=global file=lookup.ini')}}"
|
field_with_unicode: "{{lookup('ini', 'unicode', section='global', file='lookup.ini')}}"
|
||||||
- debug: var={{item}}
|
- debug: var={{item}}
|
||||||
with_items: [ 'value1_global', 'value2_global', 'value1_section1', 'field_with_unicode' ]
|
with_items: [ 'value1_global', 'value2_global', 'value1_section1', 'field_with_unicode' ]
|
||||||
- assert:
|
- assert:
|
||||||
|
|
|
@ -56,8 +56,9 @@ class TestINILookup(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_parse_parameters(self):
|
def test_parse_parameters(self):
|
||||||
|
pvals = {'file': '', 'section': '', 'key': '', 'type': '', 're': '', 'default': '', 'encoding': ''}
|
||||||
for testcase in self.old_style_params_data:
|
for testcase in self.old_style_params_data:
|
||||||
# print(testcase)
|
# print(testcase)
|
||||||
params = _parse_params(testcase['term'])
|
params = _parse_params(testcase['term'], pvals)
|
||||||
params.sort()
|
params.sort()
|
||||||
self.assertEqual(params, testcase['expected'])
|
self.assertEqual(params, testcase['expected'])
|
||||||
|
|
Loading…
Reference in a new issue