csvfile: use parse_kv() for args, add tests (#70550)

Change:
- Use parse_kv() for parsing in the csvfile lookup plugin. This allows
  us to handle multi-word search keys and filenames. Previously, the
  plugin split on space and so none of these things worked as expected.
- Add integration tests for csvfile, testing a plethora of weird cases.

Test Plan:
- New integration tests, CI

Tickets:
- Fixes #70545

Signed-off-by: Rick Elrod <rick@elrod.me>
pull/70573/head
Rick Elrod 4 years ago committed by GitHub
parent f4c89eab23
commit 1b4fd23ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,3 @@
minor_changes:
- The ``csvfile`` lookup plugin now uses ``parse_kv()`` internally. As a result, multi-word search keys can now be passed.
- The ``csvfile`` lookup plugin's documentation has been fixed; it erroneously said that the delimiter could be ``t`` which was never true. We now accept ``\t``, however, and the error in the documentation has been fixed to note that.

@ -11,16 +11,17 @@ DOCUMENTATION = """
short_description: read data from a TSV or CSV file short_description: read data from a TSV or CSV file
description: description:
- The csvfile lookup reads the contents of a file in CSV (comma-separated value) format. - The csvfile lookup reads the contents of a file in CSV (comma-separated value) format.
The lookup looks for the row where the first column matches keyname, and returns the value in the second column, unless a different column is specified. The lookup looks for the row where the first column matches keyname (which can be multiple words)
and returns the value in the C(col) column (default 1, which indexed from 0 means the second column in the file).
options: options:
col: col:
description: column to return (0 index). description: column to return (0 indexed).
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: '' default: ''
delimiter: delimiter:
description: field separator in the file, for a tab you can specify "TAB" or "t". description: field separator in the file, for a tab you can specify C(TAB) or C(\\t).
default: TAB default: TAB
file: file:
description: name of the CSV/TSV file to open. description: name of the CSV/TSV file to open.
@ -31,6 +32,10 @@ DOCUMENTATION = """
version_added: "2.1" version_added: "2.1"
notes: notes:
- The default is for TSV files (tab delimited) not CSV (comma delimited) ... yes the name is misleading. - The default is for TSV files (tab delimited) not CSV (comma delimited) ... yes the name is misleading.
- As of version 2.11, the search parameter (text that must match the first column of the file) and filename parameter can be multi-word.
- For historical reasons, in the search keyname, quotes are treated
literally and cannot be used around the string unless they appear
(escaped as required) in the first column of the file you are parsing.
""" """
EXAMPLES = """ EXAMPLES = """
@ -62,6 +67,7 @@ import codecs
import csv import csv
from ansible.errors import AnsibleError, AnsibleAssertionError from ansible.errors import AnsibleError, AnsibleAssertionError
from ansible.parsing.splitter import parse_kv
from ansible.plugins.lookup import LookupBase from ansible.plugins.lookup import LookupBase
from ansible.module_utils.six import PY2 from ansible.module_utils.six import PY2
from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils._text import to_bytes, to_native, to_text
@ -129,8 +135,12 @@ class LookupModule(LookupBase):
ret = [] ret = []
for term in terms: for term in terms:
params = term.split() kv = parse_kv(term)
key = params[0]
if '_raw_params' not in kv:
raise AnsibleError('Search key is required but was not found')
key = kv['_raw_params']
paramvals = { paramvals = {
'col': "1", # column to return 'col': "1", # column to return
@ -142,8 +152,9 @@ class LookupModule(LookupBase):
# parameters specified? # parameters specified?
try: try:
for param in params[1:]: for name, value in kv.items():
name, value = param.split('=') if name == '_raw_params':
continue
if name not in paramvals: if name not in paramvals:
raise AnsibleAssertionError('%s not in paramvals' % name) raise AnsibleAssertionError('%s not in paramvals' % name)
paramvals[name] = value paramvals[name] = value

@ -0,0 +1,2 @@
shippable/posix/group2
skip/python2.6 # lookups are controller only, and we no longer support Python 2.6 on the controller

@ -0,0 +1,3 @@
woo,i,have,spaces,in,my,filename
i,am,so,cool,haha,be,jealous
maybe,i,will,work,like,i,should
1 woo i have spaces in my filename
2 i am so cool haha be jealous
3 maybe i will work like i should

@ -0,0 +1,2 @@
this file,has,crlf,line,endings
ansible,parses,them,just,fine
1 this file has crlf line endings
2 ansible parses them just fine

@ -0,0 +1,6 @@
# Last,First,Email,Extension
Smith,Jane,jsmith@example.com,1234
Ipsum,Lorem,lipsum@another.example.com,9001
"German von Lastname",Demo,hello@example.com,123123
Example,Person,"crazy email"@example.com,9876
"""The Rock"" Johnson",Dwayne,uhoh@example.com,1337
Can't render this file because it contains an unexpected character in line 5 and column 28.

@ -0,0 +1,4 @@
fruit bananas 30
fruit apples 9
electronics tvs 8
shoes sneakers 26
1 fruit bananas 30
2 fruit apples 9
3 electronics tvs 8
4 shoes sneakers 26

@ -0,0 +1,3 @@
separatedbyx1achars
againbecause
wecan
1 separatedbyx1achars
2 againbecause
3 wecan

@ -0,0 +1,54 @@
- set_fact:
this_will_error: "{{ lookup('csvfile', 'file=people.csv delimiter=, col=1') }}"
ignore_errors: yes
register: no_keyword
- name: Make sure we failed above
assert:
that:
- no_keyword is failed
- >
"Search key is required but was not found" in no_keyword.msg
- name: Check basic comma-separated file
assert:
that:
- lookup('csvfile', 'Smith file=people.csv delimiter=, col=1') == "Jane"
- lookup('csvfile', 'German von Lastname file=people.csv delimiter=, col=1') == "Demo"
- name: Check tab-separated file
assert:
that:
- 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="\t" col=1') == "bananas"
- name: Check \x1a-separated file
assert:
that:
- lookup('csvfile', 'again file=x1a.csv delimiter=\x1a col=1') == "because"
- name: Check CSV file with CRLF line endings
assert:
that:
- lookup('csvfile', 'this file file=crlf.csv delimiter=, col=2') == "crlf"
- lookup('csvfile', 'ansible file=crlf.csv delimiter=, col=1') == "parses"
- name: Check file with multi word filename
assert:
that:
- lookup('csvfile', 'maybe file="cool list of things.csv" delimiter=, col=3') == "work"
- name: Test default behavior
assert:
that:
- lookup('csvfile', 'notfound file=people.csv delimiter=, col=2') == []
- lookup('csvfile', 'notfound file=people.csv delimiter=, col=2, default=what?') == "what?"
# NOTE: For historical reasons, this is correct; quotes in the search field must
# be treated literally as if they appear (escaped as required) in the field in the
# file. They cannot be used to surround the search text in general.
- name: Test quotes in the search field
assert:
that:
- lookup('csvfile', '"The Rock" Johnson file=people.csv delimiter=, col=1') == "Dwayne"

@ -203,6 +203,7 @@ test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/AN
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1 pslint!skip test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1 pslint!skip
test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1 pslint!skip test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1 pslint!skip
test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip
test/integration/targets/lookup_csvfile/files/crlf.csv line-endings
test/integration/targets/lookup_ini/lookup-8859-15.ini no-smart-quotes test/integration/targets/lookup_ini/lookup-8859-15.ini no-smart-quotes
test/integration/targets/module_precedence/lib_with_extension/a.ini shebang test/integration/targets/module_precedence/lib_with_extension/a.ini shebang
test/integration/targets/module_precedence/lib_with_extension/ping.ini shebang test/integration/targets/module_precedence/lib_with_extension/ping.ini shebang

Loading…
Cancel
Save