Remove the config_template module

The config_template action module has now been moved into its own git
repository (openstack/ansible-config_template). This has been done to
simplify the ability to use the plugin in other non OpenStack-Ansible
projects.

As part of this, we now need to remove the environment settings given
to ansible so that the common tests repo settings in ansible.cfg take
effect.

Related-Bug: 1791258
Depends-On: https://review.openstack.org/635841
Change-Id: I3a7d8c0c248febc4223029e076062ca68312b104
This commit is contained in:
Jesse Pretorius 2019-02-08 13:30:20 +00:00
parent 152bae8b96
commit b28590f5bd
7 changed files with 7 additions and 1073 deletions

View File

@ -1,669 +0,0 @@
# (c) 2015, Kevin Carter <kevin.carter@rackspace.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
try:
import ConfigParser
except ImportError:
import configparser as ConfigParser
import datetime
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import json
import os
import pwd
import re
import time
import yaml
import tempfile as tmpfilelib
from ansible.plugins.action import ActionBase
from ansible.module_utils._text import to_bytes, to_text
from ansible import constants as C
from ansible import errors
from ansible.parsing.yaml.dumper import AnsibleDumper
from distutils.version import LooseVersion
from ansible import __version__ as __ansible_version__
CONFIG_TYPES = {
'ini': 'return_config_overrides_ini',
'json': 'return_config_overrides_json',
'yaml': 'return_config_overrides_yaml'
}
class IDumper(AnsibleDumper):
def increase_indent(self, flow=False, indentless=False):
return super(IDumper, self).increase_indent(flow, False)
class MultiKeyDict(dict):
"""Dictionary class which supports duplicate keys.
This class allows for an item to be added into a standard python dictionary
however if a key is created more than once the dictionary will convert the
singular value to a python tuple. This tuple type forces all values to be a
string.
Example Usage:
>>> z = MultiKeyDict()
>>> z['a'] = 1
>>> z['b'] = ['a', 'b', 'c']
>>> z['c'] = {'a': 1}
>>> print(z)
... {'a': 1, 'b': ['a', 'b', 'c'], 'c': {'a': 1}}
>>> z['a'] = 2
>>> print(z)
... {'a': tuple(['1', '2']), 'c': {'a': 1}, 'b': ['a', 'b', 'c']}
"""
def __setitem__(self, key, value):
if key in self:
if isinstance(self[key], tuple):
items = self[key]
if str(value) not in items:
items += tuple([str(value)])
super(MultiKeyDict, self).__setitem__(key, items)
else:
if str(self[key]) != str(value):
items = tuple([str(self[key]), str(value)])
super(MultiKeyDict, self).__setitem__(key, items)
else:
return dict.__setitem__(self, key, value)
class ConfigTemplateParser(ConfigParser.RawConfigParser):
"""ConfigParser which supports multi key value.
The parser will use keys with multiple variables in a set as a multiple
key value within a configuration file.
Default Configuration file:
[DEFAULT]
things =
url1
url2
url3
other = 1,2,3
[section1]
key = var1
key = var2
key = var3
Example Usage:
>>> cp = ConfigTemplateParser(dict_type=MultiKeyDict)
>>> cp.read('/tmp/test.ini')
... ['/tmp/test.ini']
>>> cp.get('DEFAULT', 'things')
... \nurl1\nurl2\nurl3
>>> cp.get('DEFAULT', 'other')
... '1,2,3'
>>> cp.set('DEFAULT', 'key1', 'var1')
>>> cp.get('DEFAULT', 'key1')
... 'var1'
>>> cp.get('section1', 'key')
... {'var1', 'var2', 'var3'}
>>> cp.set('section1', 'key', 'var4')
>>> cp.get('section1', 'key')
... {'var1', 'var2', 'var3', 'var4'}
>>> with open('/tmp/test2.ini', 'w') as f:
... cp.write(f)
Output file:
[DEFAULT]
things =
url1
url2
url3
key1 = var1
other = 1,2,3
[section1]
key = var4
key = var1
key = var3
key = var2
"""
def __init__(self, *args, **kwargs):
self._comments = {}
self.ignore_none_type = bool(kwargs.pop('ignore_none_type', True))
ConfigParser.RawConfigParser.__init__(self, *args, **kwargs)
def _write(self, fp, section, key, item, entry):
if section:
# If we are not ignoring a none type value, then print out
# the option name only if the value type is None.
if not self.ignore_none_type and item is None:
fp.write(key + '\n')
elif (item is not None) or (self._optcre == self.OPTCRE):
fp.write(entry)
else:
fp.write(entry)
def _write_check(self, fp, key, value, section=False):
if isinstance(value, (tuple, set)):
for item in value:
item = str(item).replace('\n', '\n\t')
entry = "%s = %s\n" % (key, item)
self._write(fp, section, key, item, entry)
else:
if isinstance(value, list):
_value = [str(i.replace('\n', '\n\t')) for i in value]
entry = '%s = %s\n' % (key, ','.join(_value))
else:
entry = '%s = %s\n' % (key, str(value).replace('\n', '\n\t'))
self._write(fp, section, key, value, entry)
def write(self, fp):
def _write_comments(section, optname=None):
comsect = self._comments.get(section, {})
if optname in comsect:
fp.write(''.join(comsect[optname]))
if self._defaults:
_write_comments('DEFAULT')
fp.write("[%s]\n" % 'DEFAULT')
for key, value in sorted(self._defaults.items()):
_write_comments('DEFAULT', optname=key)
self._write_check(fp, key=key, value=value)
else:
fp.write("\n")
for section in sorted(self._sections):
_write_comments(section)
fp.write("[%s]\n" % section)
for key, value in sorted(self._sections[section].items()):
_write_comments(section, optname=key)
self._write_check(fp, key=key, value=value, section=True)
else:
fp.write("\n")
def _read(self, fp, fpname):
comments = []
cursect = None
optname = None
lineno = 0
e = None
while True:
line = fp.readline()
if not line:
break
lineno += 1
if line.strip() == '':
if comments:
comments.append('')
continue
if line[0] in '#;':
comments.append(line)
continue
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
continue
if line[0].isspace() and cursect is not None and optname:
value = line.strip()
if value:
if isinstance(cursect[optname], (tuple, set)):
_temp_item = list(cursect[optname])
del cursect[optname]
cursect[optname] = _temp_item
elif isinstance(cursect[optname], (str, unicode)):
_temp_item = [cursect[optname]]
del cursect[optname]
cursect[optname] = _temp_item
cursect[optname].append(value)
else:
mo = self.SECTCRE.match(line)
if mo:
sectname = mo.group('header')
if sectname in self._sections:
cursect = self._sections[sectname]
elif sectname == 'DEFAULT':
cursect = self._defaults
else:
cursect = self._dict()
self._sections[sectname] = cursect
optname = None
comsect = self._comments.setdefault(sectname, {})
if comments:
# NOTE(flaper87): Using none as the key for
# section level comments
comsect[None] = comments
comments = []
elif cursect is None:
raise ConfigParser.MissingSectionHeaderError(
fpname,
lineno,
line
)
else:
mo = self._optcre.match(line)
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
optname = self.optionxform(optname.rstrip())
if optval is not None:
if vi in ('=', ':') and ';' in optval:
pos = optval.find(';')
if pos != -1 and optval[pos - 1].isspace():
optval = optval[:pos]
optval = optval.strip()
if optval == '""':
optval = ''
cursect[optname] = optval
if comments:
comsect[optname] = comments
comments = []
else:
if not e:
e = ConfigParser.ParsingError(fpname)
e.append(lineno, repr(line))
if e:
raise e
all_sections = [self._defaults]
all_sections.extend(self._sections.values())
for options in all_sections:
for name, val in options.items():
if isinstance(val, list):
_temp_item = '\n'.join(val)
del options[name]
options[name] = _temp_item
class ActionModule(ActionBase):
TRANSFERS_FILES = True
def return_config_overrides_ini(self,
config_overrides,
resultant,
list_extend=True,
ignore_none_type=True):
"""Returns string value from a modified config file.
:param config_overrides: ``dict``
:param resultant: ``str`` || ``unicode``
:returns: ``str``
"""
# If there is an exception loading the RawConfigParser The config obj
# is loaded again without the extra option. This is being done to
# support older python.
try:
config = ConfigTemplateParser(
allow_no_value=True,
dict_type=MultiKeyDict,
ignore_none_type=ignore_none_type
)
config.optionxform = str
except Exception:
config = ConfigTemplateParser(dict_type=MultiKeyDict)
config_object = StringIO(resultant)
config.readfp(config_object)
for section, items in config_overrides.items():
# If the items value is not a dictionary it is assumed that the
# value is a default item for this config type.
if not isinstance(items, dict):
if isinstance(items, list):
items = ','.join(to_text(i) for i in items)
self._option_write(
config,
'DEFAULT',
section,
items
)
else:
# Attempt to add a section to the config file passing if
# an error is raised that is related to the section
# already existing.
try:
config.add_section(section)
except (ConfigParser.DuplicateSectionError, ValueError):
pass
for key, value in items.items():
try:
self._option_write(config, section, key, value)
except ConfigParser.NoSectionError as exp:
error_msg = str(exp)
error_msg += (
' Try being more explicit with your override'
'data. Sections are case sensitive.'
)
raise errors.AnsibleModuleError(error_msg)
else:
config_object.close()
resultant_stringio = StringIO()
try:
config.write(resultant_stringio)
return resultant_stringio.getvalue()
finally:
resultant_stringio.close()
@staticmethod
def _option_write(config, section, key, value):
config.remove_option(str(section), str(key))
try:
if not any(list(value.values())):
value = tuple(value.keys())
except AttributeError:
pass
if isinstance(value, (tuple, set)):
config.set(str(section), str(key), value)
elif isinstance(value, set):
config.set(str(section), str(key), value)
elif isinstance(value, list):
config.set(str(section), str(key), ','.join(str(i) for i in value))
else:
config.set(str(section), str(key), str(value))
def return_config_overrides_json(self,
config_overrides,
resultant,
list_extend=True,
ignore_none_type=True):
"""Returns config json
Its important to note that file ordering will not be preserved as the
information within the json file will be sorted by keys.
:param config_overrides: ``dict``
:param resultant: ``str`` || ``unicode``
:returns: ``str``
"""
original_resultant = json.loads(resultant)
merged_resultant = self._merge_dict(
base_items=original_resultant,
new_items=config_overrides,
list_extend=list_extend
)
return json.dumps(
merged_resultant,
indent=4,
sort_keys=True
)
def return_config_overrides_yaml(self,
config_overrides,
resultant,
list_extend=True,
ignore_none_type=True):
"""Return config yaml.
:param config_overrides: ``dict``
:param resultant: ``str`` || ``unicode``
:returns: ``str``
"""
original_resultant = yaml.safe_load(resultant)
merged_resultant = self._merge_dict(
base_items=original_resultant,
new_items=config_overrides,
list_extend=list_extend
)
return yaml.dump(
merged_resultant,
Dumper=IDumper,
default_flow_style=False,
width=1000,
)
def _merge_dict(self, base_items, new_items, list_extend=True):
"""Recursively merge new_items into base_items.
:param base_items: ``dict``
:param new_items: ``dict``
:returns: ``dict``
"""
for key, value in new_items.items():
if isinstance(value, dict):
base_items[key] = self._merge_dict(
base_items=base_items.get(key, {}),
new_items=value,
list_extend=list_extend
)
elif not isinstance(value, int) and (',' in value or '\n' in value):
base_items[key] = re.split(',|\n', value)
base_items[key] = [i.strip() for i in base_items[key] if i]
elif isinstance(value, list):
if isinstance(base_items.get(key), list) and list_extend:
base_items[key].extend(value)
else:
base_items[key] = value
elif isinstance(value, (tuple, set)):
if isinstance(base_items.get(key), tuple) and list_extend:
base_items[key] += tuple(value)
elif isinstance(base_items.get(key), list) and list_extend:
base_items[key].extend(list(value))
else:
base_items[key] = value
else:
base_items[key] = new_items[key]
return base_items
def _load_options_and_status(self, task_vars):
"""Return options and status from module load."""
config_type = self._task.args.get('config_type')
if config_type not in ['ini', 'yaml', 'json']:
return False, dict(
failed=True,
msg="No valid [ config_type ] was provided. Valid options are"
" ini, yaml, or json."
)
# Access to protected method is unavoidable in Ansible
searchpath = [self._loader._basedir]
if self._task._role:
file_path = self._task._role._role_path
searchpath.insert(1, C.DEFAULT_ROLES_PATH)
searchpath.insert(1, self._task._role._role_path)
else:
file_path = self._loader.get_basedir()
user_source = self._task.args.get('src')
# (alextricity25) It's possible that the user could pass in a datatype
# and not always a string. In this case we don't want the datatype
# python representation to be printed out to the file, but rather we
# want the serialized version.
_user_content = self._task.args.get('content')
# If the data type of the content input is a dictionary, it's
# converted dumped as json if config_type is 'json'.
if isinstance(_user_content, dict):
if self._task.args.get('config_type') == 'json':
_user_content = json.dumps(_user_content)
user_content = str(_user_content)
if not user_source:
if not user_content:
return False, dict(
failed=True,
msg="No user [ src ] or [ content ] was provided"
)
else:
tmp_content = None
fd, tmp_content = tmpfilelib.mkstemp()
try:
with open(tmp_content, 'wb') as f:
f.write(user_content.encode())
except Exception as err:
os.remove(tmp_content)
raise Exception(err)
self._task.args['src'] = source = tmp_content
else:
source = self._loader.path_dwim_relative(
file_path,
'templates',
user_source
)
searchpath.insert(1, os.path.dirname(source))
_dest = self._task.args.get('dest')
list_extend = self._task.args.get('list_extend')
if not _dest:
return False, dict(
failed=True,
msg="No [ dest ] was provided"
)
else:
# Expand any user home dir specification
user_dest = self._remote_expand_user(_dest)
if user_dest.endswith(os.sep):
user_dest = os.path.join(user_dest, os.path.basename(source))
# Get ignore_none_type
# In some situations(i.e. my.cnf files), INI files can have valueless
# options that don't have a '=' or ':' suffix. In these cases,
# ConfigParser gives these options a "None" value. If ignore_none_type
# is set to true, these key/value options will be ignored, if it's set
# to false, then ConfigTemplateParser will write out only the option
# name with out the '=' or ':' suffix. The default is true.
ignore_none_type = self._task.args.get('ignore_none_type', True)
return True, dict(
source=source,
dest=user_dest,
config_overrides=self._task.args.get('config_overrides', dict()),
config_type=config_type,
searchpath=searchpath,
list_extend=list_extend,
ignore_none_type=ignore_none_type
)
def run(self, tmp=None, task_vars=None):
"""Run the method"""
try:
remote_user = task_vars.get('ansible_user')
if not remote_user:
remote_user = task_vars.get('ansible_ssh_user')
if not remote_user:
remote_user = self._play_context.remote_user
if not tmp:
tmp = self._make_tmp_path(remote_user)
except TypeError:
if not tmp:
tmp = self._make_tmp_path()
_status, _vars = self._load_options_and_status(task_vars=task_vars)
if not _status:
return _vars
temp_vars = task_vars.copy()
template_host = temp_vars['template_host'] = os.uname()[1]
source = temp_vars['template_path'] = _vars['source']
temp_vars['template_mtime'] = datetime.datetime.fromtimestamp(
os.path.getmtime(source)
)
try:
template_uid = temp_vars['template_uid'] = pwd.getpwuid(
os.stat(source).st_uid
).pw_name
except Exception:
template_uid = temp_vars['template_uid'] = os.stat(source).st_uid
managed_default = C.DEFAULT_MANAGED_STR
managed_str = managed_default.format(
host=template_host,
uid=template_uid,
file=to_bytes(source)
)
temp_vars['ansible_managed'] = time.strftime(
managed_str,
time.localtime(os.path.getmtime(source))
)
temp_vars['template_fullpath'] = os.path.abspath(source)
temp_vars['template_run_date'] = datetime.datetime.now()
with open(source, 'r') as f:
template_data = to_text(f.read())
self._templar.environment.loader.searchpath = _vars['searchpath']
self._templar.set_available_variables(temp_vars)
resultant = self._templar.template(
template_data,
preserve_trailing_newlines=True,
escape_backslashes=False,
convert_data=False
)
# Access to protected method is unavoidable in Ansible
self._templar.set_available_variables(
self._templar._available_variables
)
if _vars['config_overrides']:
type_merger = getattr(self, CONFIG_TYPES.get(_vars['config_type']))
resultant = type_merger(
config_overrides=_vars['config_overrides'],
resultant=resultant,
list_extend=_vars.get('list_extend', True),
ignore_none_type=_vars.get('ignore_none_type', True)
)
# Re-template the resultant object as it may have new data within it
# as provided by an override variable.
resultant = self._templar.template(
resultant,
preserve_trailing_newlines=True,
escape_backslashes=False,
convert_data=False
)
# run the copy module
new_module_args = self._task.args.copy()
# Access to protected method is unavoidable in Ansible
transferred_data = self._transfer_data(
self._connection._shell.join_path(tmp, 'source'),
resultant
)
if LooseVersion(__ansible_version__) < LooseVersion("2.6"):
new_module_args.update(
dict(
src=transferred_data,
dest=_vars['dest'],
original_basename=os.path.basename(source),
follow=True,
),
)
else:
new_module_args.update(
dict(
src=transferred_data,
dest=_vars['dest'],
_original_basename=os.path.basename(source),
follow=True,
),
)
# Remove data types that are not available to the copy module
new_module_args.pop('config_overrides', None)
new_module_args.pop('config_type', None)
new_module_args.pop('list_extend', None)
new_module_args.pop('ignore_none_type', None)
# Content from config_template is converted to src
new_module_args.pop('content', None)
# Run the copy module
rc = self._execute_module(
module_name='copy',
module_args=new_module_args,
task_vars=task_vars
)
if self._task.args.get('content'):
os.remove(_vars['source'])
return rc

View File

@ -1,22 +0,0 @@
config_template
~~~~~~~~~~~~~~~
Synopsis
--------
Renders template files providing a create/update override interface
- The module contains the template functionality with the ability to override
items in config, in transit, through the use of a simple dictionary without
having to write out various temp files on target machines. The module renders
all of the potential jinja a user could provide in both the template file and
in the override dictionary which is ideal for deployers who may have lots of
different configs using a similar code base.
- The module is an extension of the **copy** module and all of attributes that
can be set there are available to be set here.
Examples
--------
.. literalinclude:: ../../../library/config_template
:language: yaml
:start-after: EXAMPLES = """
:end-before: """

View File

@ -1,86 +0,0 @@
# this is a virtual module that is entirely implemented server side
DOCUMENTATION = """
---
module: config_template
version_added: 1.9.2
short_description: Renders template files providing a create/update override interface
description:
- The module contains the template functionality with the ability to override items
in config, in transit, through the use of a simple dictionary without having to
write out various temp files on target machines. The module renders all of the
potential jinja a user could provide in both the template file and in the override
dictionary which is ideal for deployers who may have lots of different configs
using a similar code base.
- The module is an extension of the **copy** module and all of attributes that can be
set there are available to be set here.
options:
src:
description:
- Path of a Jinja2 formatted template on the local server. This can be a relative
or absolute path.
required: true
default: null
dest:
description:
- Location to render the template to on the remote machine.
required: true
default: null
config_overrides:
description:
- A dictionary used to update or override items within a configuration template.
The dictionary data structure may be nested. If the target config file is an ini
file the nested keys in the ``config_overrides`` will be used as section
headers.
config_type:
description:
- A string value describing the target config type.
choices:
- ini
- json
- yaml
list_extend:
description:
- By default a list item in a JSON or YAML format will extend if
its already defined in the target template and a config_override
using a list is being set for the existing "key". This functionality
can be toggled on or off using this option. If disabled an override
list will replace an existing "key".
choices:
- True
- False
ignore_none_type:
description:
- Can be true or false. If ignore_none_type is set to true, then
valueless INI options will not be written out to the resultant file.
If it's set to false, then config_template will write out only
the option name without the '=' or ':' suffix. The default is true.
choices:
- True
- False
author: Kevin Carter
"""
EXAMPLES = """
- name: run config template ini
config_template:
src: templates/test.ini.j2
dest: /tmp/test.ini
config_overrides: {}
config_type: ini
- name: run config template json
config_template:
src: templates/test.json.j2
dest: /tmp/test.json
config_overrides: {}
config_type: json
- name: run config template yaml
config_template:
src: templates/test.yaml.j2
dest: /tmp/test.yaml
config_overrides: {}
config_type: yaml
"""

View File

@ -0,0 +1,7 @@
---
other:
- |
The ``config_template`` action module has now been moved into its own git
repository (``openstack/ansible-config_template``). This has been done to
simplify the ability to use the plugin in other non OpenStack-Ansible
projects.

View File

@ -1,287 +0,0 @@
---
# Copyright 2016, Comcast Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
- name: Test config_template
hosts: localhost
connection: local
gather_facts: yes
tasks:
# Test basic function of config_template
- name: Template test INI template
config_template:
src: "{{ playbook_dir }}/templates/test.ini"
dest: "/tmp/test.ini"
config_overrides: "{{ test_config_ini_overrides }}"
config_type: "ini"
- name: Read test.ini
slurp:
src: /tmp/test.ini
register: ini_file
- debug:
msg: "ini - {{ ini_file.content | b64decode }}"
- name: Validate output
assert:
that:
- "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test.ini')) == 'new_value'"
- "(lookup('ini', 'baz section=foo file=/tmp/test.ini')) == 'bar'"
# Test basic function of config_template with content instead of src
- name: Template test INI template
config_template:
content: "{{ lookup('file', playbook_dir + '/templates/test.ini') }}"
dest: "/tmp/test_with_content.ini"
config_overrides: "{{ test_config_ini_overrides }}"
config_type: "ini"
- name: Read test.ini
slurp:
src: /tmp/test_with_content.ini
register: ini_file_with_content
- debug:
msg: "ini - {{ ini_file_with_content.content | b64decode }}"
- name: Validate output
assert:
that:
- "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test_with_content.ini')) == 'new_value'"
- "(lookup('ini', 'baz section=foo file=/tmp/test_with_content.ini')) == 'bar'"
# Test list additions in config_template
- name: Template test YML template
config_template:
src: "{{ playbook_dir }}/templates/test.yml"
dest: "/tmp/test_extend.yml"
config_overrides: "{{ test_config_yml_overrides }}"
config_type: "yaml"
list_extend: True
- name: Read test_extend.yml
slurp:
src: /tmp/test_extend.yml
register: extend_file
- name: Read expected test_extend.yml
slurp:
src: "{{ playbook_dir }}/files/test_extend.yml.expected"
register: extend_file_expected
- debug:
msg: "extend - {{ extend_file.content | b64decode }}"
- debug:
msg: "extend.expected - {{ extend_file_expected.content | b64decode }}"
- name: Compare files
assert:
that:
- "(extend_file.content | b64decode) == (extend_file_expected.content | b64decode)"
# Test list replacement in config_template
- name: Template test YML template
config_template:
src: "{{ playbook_dir }}/templates/test.yml"
dest: "/tmp/test_no_extend.yml"
config_overrides: "{{ test_config_yml_overrides }}"
config_type: "yaml"
list_extend: False
- name: Read test_no_extend.yml
slurp:
src: /tmp/test_no_extend.yml
register: no_extend_file
- name: Read expected test_no_extend.yml
slurp:
src: "{{ playbook_dir }}/files/test_no_extend.yml.expected"
register: no_extend_file_expected
- debug:
msg: "no_extend - {{ no_extend_file.content | b64decode }}"
- debug:
msg: "no_extend.expected - {{ no_extend_file_expected.content | b64decode }}"
- name: Compare files
assert:
that:
- "(no_extend_file.content | b64decode) == (no_extend_file_expected.content | b64decode)"
# Test dumping hostvars using config overrides
- name: Template test YML template with hostvars override
config_template:
src: "{{ playbook_dir }}/templates/test.yml"
dest: "/tmp/test_hostvars.yml"
config_overrides: "{{ test_config_yml_hostvars_overrides }}"
config_type: "yaml"
- name: Read test_hostvars.yml
slurp:
src: /tmp/test_hostvars.yml
register: hostvars_file
- debug:
msg: "hostvars - {{ (hostvars_file.content | b64decode | from_yaml).test_hostvar }}"
- debug:
msg: "hostvars.expected - {{ test_config_yml_hostvars_overrides.test_hostvar }}"
- name: Compare files
assert:
that:
- "((hostvars_file.content | b64decode | from_yaml).test_hostvar) == (test_config_yml_hostvars_overrides.test_hostvar)"
# Test multistropt ordering
- name: Template MultiStrOpts using overrides
config_template:
src: test_multistropts.ini
dest: /tmp/test_multistropts.ini
config_overrides:
testsection:
test: output
config_type: ini
- name: Read test_multistropts.ini
slurp:
src: /tmp/test_multistropts.ini
register: multistropts_file
- name: Read test_multistropts.ini.expected
slurp:
src: files/test_multistropts.ini.expected
register: multistropts_expected_file
- debug:
msg: "multistropts rendered - {{ multistropts_file.content | b64decode }}"
- debug:
msg: "multistropts expected - {{ multistropts_expected_file.content | b64decode }}"
- name: Compare files
assert:
that:
- "multistropts_file.content == multistropts_expected_file.content"
# Test content attribute with a dictionary input and config_type equal to 'json'
- name: Template test JSON template with content attribute
config_template:
dest: "/tmp/test_content_no_overrides.json"
config_overrides: {}
config_type: "json"
content: "{{ lookup('file', playbook_dir ~ '/templates/test.json') | from_json }}"
- name: Read test_content_no_overrides.json
slurp:
src: /tmp/test_content_no_overrides.json
register: content_no_overrides_file
- name: Read expected test_content_no_overrides.json
slurp:
src: "{{ playbook_dir }}/files/test_content_no_overrides.json.expected"
register: content_no_overrides_file_expected
- debug:
msg: "content_no_overrides.json - {{ content_no_overrides_file.content | b64decode | from_json }}"
- debug:
msg: "content_no_overrides.json.expected - {{ content_no_overrides_file_expected.content | b64decode | from_json }}"
# NOTE (alextricity25): The config_template module doesn't use ordered dicts when reading and writing json
# data, so we can't guarantee that the string literal of both file's content will be the same. Instead, we compare
# the content after transforming it into a dictionary.
- name: Compare file content
assert:
that:
- "(content_no_overrides_file.content | b64decode | from_json) == (content_no_overrides_file_expected.content | b64decode | from_json)"
# Test the ignore_none_type attribute when set to False
- name: Template test with ignore_none_type set to false
config_template:
src: "{{ playbook_dir }}/templates/test_ignore_none_type.ini"
dest: "/tmp/test_ignore_none_type.ini"
config_overrides: "{{ test_config_ini_overrides }}"
config_type: "ini"
ignore_none_type: False
- name: Read test_ignore_none_type.ini
slurp:
src: /tmp/test_ignore_none_type.ini
register: test_ignore_none_type
- debug:
msg: "test_ignore_none_type.ini - {{ test_ignore_none_type.content | b64decode }}"
- name: Validate output has valueless options printed out
assert:
that:
- "{{ test_ignore_none_type.content | b64decode | search('(?m)^india$') }}"
- "{{ test_ignore_none_type.content | b64decode | search('(?m)^juliett kilo$') }}"
# Test basic function of config_template
- name: Template test INI comments
config_template:
src: "{{ playbook_dir }}/templates/test_with_comments.ini"
dest: "/tmp/test_with_comments.ini"
config_overrides: "{{ test_config_ini_overrides }}"
config_type: "ini"
tags: test
- name: Read test.ini
slurp:
src: /tmp/test_with_comments.ini
register: ini_file
tags: test
- debug:
msg: "ini - {{ ini_file.content | b64decode }}"
- name: Validate output
tags: test
assert:
that:
- "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test_with_comments.ini')) == 'new_value'"
- "(lookup('ini', 'baz section=foo file=/tmp/test_with_comments.ini')) == 'bar'"
- "{{ ini_file.content | b64decode | search('#This is a comment')}}"
- "{{ ini_file.content | b64decode | search('# A default section comment\n# broken into multiple lines\n\\[DEFAULT\\]')}}"
- name: Template multiple times to assert no changes
config_template:
src: "{{ playbook_dir }}/templates/test_with_comments.ini"
dest: "/tmp/test_with_comments.ini"
config_type: "ini"
config_overrides: "{{ item[1] }}"
register: template_changed
failed_when: template_changed is changed
with_nested:
- [ 0, 1, 2 ]
- [ "{{ test_config_ini_overrides }}" ]
vars:
test_config_ini_overrides:
DEFAULT:
new_key: "new_value"
foo:
baz: "bar"
section1:
key1: "value1"
key2: "value2"
key3: "value3"
key4: "value4"
key5: "value5"
key6: "value6"
key7: "value7"
key8: "value8"
key9: "value9"
key10: "value10"
key11: "value11"
section2:
key1: "value1"
section3:
key1: "value1"
section4:
key1: "value1"
section5:
key1: "value1"
section6:
key1: "value1"
section7:
key1: "value1"
section8:
key1: "value1"
section9:
key1: "value1"
section10:
key1: "value1"
section11:
key1: "value1"
test_config_yml_overrides:
list_one:
- four
test_config_yml_hostvars_overrides:
test_hostvar: "{{ ansible_default_ipv4.address }}"

View File

@ -19,8 +19,6 @@
- import_playbook: common/test-setup-host.yml
- import_playbook: test-config_template.yml
- import_playbook: test-filters.yml
- import_playbook: test-lookups.yml

View File

@ -29,13 +29,6 @@ setenv =
TEST_IDEMPOTENCE=false
VIRTUAL_ENV={envdir}
WORKING_DIR={toxinidir}
ANSIBLE_ACTION_PLUGINS={toxinidir}/action
ANSIBLE_CALLBACK_PLUGINS={toxinidir}/callback
ANSIBLE_CONNECTION_PLUGINS={toxinidir}/connection
ANSIBLE_FILTER_PLUGINS={toxinidir}/filter
ANSIBLE_LOOKUP_PLUGINS={toxinidir}/lookup
ANSIBLE_STRATEGY_PLUGINS={toxinidir}/strategy
ANSIBLE_LIBRARY={toxinidir}/library
[testenv:docs]