Fact diff (#15)
* Add fact_diff action * Formatting * Update docs * Add skip_lines * Update docs * black * bug fix * Add units for fact_diff * Initial changes to support plugin architecture * Restructure docstring * Integration test fix * Wrap diff plugin with try/except * Fix integration test * Restructure code to allow for plugin failure test * Examples, docs * Fix debug statement * Update examples * Minor doc updates * Minor doc updates * Minor doc updates * Add change log Co-authored-by: cidrblock <brad@thethorntons.net>pull/19/head
parent
a727928e3d
commit
c20ba34c7d
|
@ -35,6 +35,7 @@ Name | Description
|
||||||
### Modules
|
### Modules
|
||||||
Name | Description
|
Name | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
|
[ansible.utils.fact_diff](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.fact_diff_module.rst)|Find the difference between currently set facts
|
||||||
[ansible.utils.update_fact](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.update_fact_module.rst)|Update currently set facts
|
[ansible.utils.update_fact](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.update_fact_module.rst)|Update currently set facts
|
||||||
|
|
||||||
<!--end collection content-->
|
<!--end collection content-->
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
minor_changes:
|
||||||
|
- Add fact_diff module. Find the difference between text, files or facts
|
|
@ -0,0 +1,340 @@
|
||||||
|
.. _ansible.utils.fact_diff_module:
|
||||||
|
|
||||||
|
|
||||||
|
***********************
|
||||||
|
ansible.utils.fact_diff
|
||||||
|
***********************
|
||||||
|
|
||||||
|
**Find the difference between currently set facts**
|
||||||
|
|
||||||
|
|
||||||
|
Version added: 1.0.0
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
:depth: 1
|
||||||
|
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
- Compare two facts or variables and get a diff
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<table border=0 cellpadding=0 class="documentation-table">
|
||||||
|
<tr>
|
||||||
|
<th colspan="3">Parameter</th>
|
||||||
|
<th>Choices/<font color="blue">Defaults</font></th>
|
||||||
|
<th width="100%">Comments</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>after</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">raw</span>
|
||||||
|
/ <span style="color: red">required</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>The second fact to be used in the comparison</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>before</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">raw</span>
|
||||||
|
/ <span style="color: red">required</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>The first fact to be used in the comparison</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>plugin</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">dictionary</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>Default:</b><br/><div style="color: blue">{}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Configure and specify the diff plugin to use</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>name</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">string</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>Default:</b><br/><div style="color: blue">"ansible.utils.native"</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>The diff plugin to use, in collection format</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>vars</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">dictionary</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>Default:</b><br/><div style="color: blue">{}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Parameters passed to the diff plugin</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td colspan="1">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>skip_lines</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">list</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Skip lines matching these regular expressions</div>
|
||||||
|
<div>Matches will be removed prior to the diff</div>
|
||||||
|
<div>If the provided <em>before</em> and <em>after</em> are a string, they will be split</div>
|
||||||
|
<div>Each entry in each list will be cast to a string for the comparison</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
before:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
after:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
|
||||||
|
- name: Show the difference in json format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before }}"
|
||||||
|
after: "{{ after }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3,8 +3,8 @@
|
||||||
|
# "b": {
|
||||||
|
# "c": {
|
||||||
|
# "d": [
|
||||||
|
# - 0,
|
||||||
|
# - 1
|
||||||
|
# + 2,
|
||||||
|
# + 3
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in path format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|ansible.utils.to_paths }}"
|
||||||
|
after: "{{ after|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -1,4 +1,4 @@
|
||||||
|
# {
|
||||||
|
# - "a.b.c.d[0]": 0,
|
||||||
|
# - "a.b.c.d[1]": 1
|
||||||
|
# + "a.b.c.d[0]": 2,
|
||||||
|
# + "a.b.c.d[1]": 3
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in yaml format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|to_nice_yaml }}"
|
||||||
|
after: "{{ after|to_nice_yaml }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -2,5 +2,5 @@
|
||||||
|
# b:
|
||||||
|
# c:
|
||||||
|
# d:
|
||||||
|
# - - 0
|
||||||
|
# - - 1
|
||||||
|
# + - 2
|
||||||
|
# + - 3
|
||||||
|
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in yaml format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before }}"
|
||||||
|
after: "{{ before }}"
|
||||||
|
|
||||||
|
|
||||||
|
#### Show the difference between complex object using restconf
|
||||||
|
# ansible_connection: ansible.netcommon.httpapi
|
||||||
|
# ansible_httpapi_use_ssl: True
|
||||||
|
# ansible_httpapi_validate_certs: False
|
||||||
|
# ansible_network_os: ansible.netcommon.restconf
|
||||||
|
|
||||||
|
- name: Get the current interface config prior ro changes
|
||||||
|
ansible.netcommon.restconf_get:
|
||||||
|
content: config
|
||||||
|
path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
register: pre
|
||||||
|
|
||||||
|
- name: Update the description of eth1/100
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: "pre['response']['phys-items']['PhysIf-list'][{{ index }}]['descr']"
|
||||||
|
value: "Configured by ansible {{ 100 | random }}"
|
||||||
|
vars:
|
||||||
|
index: "{{ pre['response']['phys-items']['PhysIf-list']|ansible.utils.index_of('eq', 'eth1/100', 'id') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Apply the configuration
|
||||||
|
ansible.netcommon.restconf_config:
|
||||||
|
path: 'data/Cisco-NX-OS-device:System/intf-items/'
|
||||||
|
content: "{{ updated.pre.response}}"
|
||||||
|
method: patch
|
||||||
|
|
||||||
|
- name: Get the current interface config after changes
|
||||||
|
ansible.netcommon.restconf_get:
|
||||||
|
content: config
|
||||||
|
path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
register: post
|
||||||
|
|
||||||
|
- name: Show the difference
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ pre.response|ansible.utils.to_paths }}"
|
||||||
|
after: "{{ post.response|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] *********************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3604,7 +3604,7 @@
|
||||||
|
# "phys-items['PhysIf-list'][37].bw": "0",
|
||||||
|
# "phys-items['PhysIf-list'][37].controllerId": "",
|
||||||
|
# "phys-items['PhysIf-list'][37].delay": "1",
|
||||||
|
# - "phys-items['PhysIf-list'][37].descr": "Configured by ansible 95",
|
||||||
|
# + "phys-items['PhysIf-list'][37].descr": "Configured by ansible 20",
|
||||||
|
# "phys-items['PhysIf-list'][37].dot1qEtherType": "0x8100",
|
||||||
|
# "phys-items['PhysIf-list'][37].duplex": "auto",
|
||||||
|
# "phys-items['PhysIf-list'][37].id": "eth1/100",
|
||||||
|
|
||||||
|
# changed: [nxos101]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Return Values
|
||||||
|
-------------
|
||||||
|
Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<table border=0 cellpadding=0 class="documentation-table">
|
||||||
|
<tr>
|
||||||
|
<th colspan="1">Key</th>
|
||||||
|
<th>Returned</th>
|
||||||
|
<th width="100%">Description</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="1">
|
||||||
|
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||||
|
<b>diff_lines</b>
|
||||||
|
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">list</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>always</td>
|
||||||
|
<td>
|
||||||
|
<div>The <code>diff_text</code> split into lines</div>
|
||||||
|
<br/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="1">
|
||||||
|
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||||
|
<b>diff_text</b>
|
||||||
|
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">string</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>always</td>
|
||||||
|
<td>
|
||||||
|
<div>The diff in text format</div>
|
||||||
|
<br/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
|
||||||
|
Status
|
||||||
|
------
|
||||||
|
|
||||||
|
|
||||||
|
Authors
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
- Bradley Thornton (@cidrblock)
|
|
@ -0,0 +1,139 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyafter 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
import re
|
||||||
|
from importlib import import_module
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.errors import AnsibleActionFail
|
||||||
|
from ansible.module_utils._text import to_native
|
||||||
|
from ansible_collections.ansible.utils.plugins.modules.fact_diff import (
|
||||||
|
DOCUMENTATION,
|
||||||
|
)
|
||||||
|
from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
|
||||||
|
AnsibleArgSpecValidator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ActionModule(ActionBase):
|
||||||
|
"""action module"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ActionModule, self).__init__(*args, **kwargs)
|
||||||
|
self._supports_async = True
|
||||||
|
self._task_vars = None
|
||||||
|
|
||||||
|
def _check_argspec(self):
|
||||||
|
aav = AnsibleArgSpecValidator(
|
||||||
|
data=self._task.args,
|
||||||
|
schema=DOCUMENTATION,
|
||||||
|
schema_format="doc",
|
||||||
|
name=self._task.action,
|
||||||
|
)
|
||||||
|
valid, errors, self._task.args = aav.validate()
|
||||||
|
if not valid:
|
||||||
|
self._result["failed"] = True
|
||||||
|
self._result["msg"] = errors
|
||||||
|
|
||||||
|
def _debug(self, msg):
|
||||||
|
"""Output text using ansible's display
|
||||||
|
|
||||||
|
:param msg: The message
|
||||||
|
:type msg: str
|
||||||
|
"""
|
||||||
|
msg = "<{phost}> [fact_diff][{plugin}] {msg}".format(
|
||||||
|
phost=self._playhost, plugin=self._plugin, msg=msg
|
||||||
|
)
|
||||||
|
self._display.vvvv(msg)
|
||||||
|
|
||||||
|
def _load_plugin(self, plugin, directory, class_name):
|
||||||
|
"""Load a plugin from the fs
|
||||||
|
|
||||||
|
:param plugin: The name of the plugin in collection format
|
||||||
|
:type plugin: string
|
||||||
|
:param directory: The name of the plugin directory to use
|
||||||
|
:type directory: string
|
||||||
|
:param class_name: The name of the class to load from the plugin
|
||||||
|
:type class_name: string
|
||||||
|
:return: An instance of class class_name
|
||||||
|
:rtype: class_name
|
||||||
|
"""
|
||||||
|
if len(plugin.split(".")) != 3:
|
||||||
|
msg = "Plugin name should be provided as a full name including collection"
|
||||||
|
self._result["failed"] = True
|
||||||
|
self._result["msg"] = msg
|
||||||
|
return None
|
||||||
|
cref = dict(zip(["corg", "cname", "plugin"], plugin.split(".")))
|
||||||
|
cref.update(directory=directory)
|
||||||
|
parserlib = "ansible_collections.{corg}.{cname}.plugins.{directory}.{plugin}".format(
|
||||||
|
**cref
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
class_obj = getattr(import_module(parserlib), class_name)
|
||||||
|
class_instance = class_obj(
|
||||||
|
task_args=self._task.args,
|
||||||
|
task_vars=self._task_vars,
|
||||||
|
debug=self._debug,
|
||||||
|
)
|
||||||
|
return class_instance
|
||||||
|
except Exception as exc:
|
||||||
|
self._result["failed"] = True
|
||||||
|
self._result[
|
||||||
|
"msg"
|
||||||
|
] = "Error loading plugin '{plugin}': {err}".format(
|
||||||
|
plugin=plugin, err=to_native(exc)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _run_diff(self, plugin_instance):
|
||||||
|
try:
|
||||||
|
result = plugin_instance.diff()
|
||||||
|
if "errors" in result:
|
||||||
|
self._result["failed"] = True
|
||||||
|
self._result["msg"] = result["errors"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
msg = "Unhandled exception from plugin '{plugin}'. Error: {err}".format(
|
||||||
|
plugin=self._task.args["plugin"]["name"], err=to_native(exc)
|
||||||
|
)
|
||||||
|
self._result["failed"] = True
|
||||||
|
self._result["msg"] = msg
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(self, tmp=None, task_vars=None):
|
||||||
|
self._task.diff = True
|
||||||
|
self._result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
|
self._task_vars = task_vars
|
||||||
|
self._playhost = task_vars.get("inventory_hostname")
|
||||||
|
|
||||||
|
self._check_argspec()
|
||||||
|
if self._result.get("failed"):
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
self._plugin = self._task.args["plugin"]["name"]
|
||||||
|
plugin_instance = self._load_plugin(
|
||||||
|
self._plugin, "fact_diff", "FactDiff"
|
||||||
|
)
|
||||||
|
if self._result.get("failed"):
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
result = self._run_diff(plugin_instance)
|
||||||
|
if self._result.get("failed"):
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
|
||||||
|
diff_text = ansi_escape.sub("", result["diff"])
|
||||||
|
self._result.update(
|
||||||
|
{
|
||||||
|
"diff": {"prepared": result["diff"]},
|
||||||
|
"changed": bool(result["diff"]),
|
||||||
|
"diff_lines": diff_text.splitlines(),
|
||||||
|
"diff_text": diff_text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._result
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyafter 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import re
|
||||||
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
from ansible_collections.ansible.utils.plugins.module_utils.base_classes.fact_diff import (
|
||||||
|
FactDiffBase,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FactDiff(FactDiffBase):
|
||||||
|
def _check_valid_regexes(self):
|
||||||
|
if self._skip_lines:
|
||||||
|
self._debug("Checking regex in 'split_lines' for validity")
|
||||||
|
for idx, regex in enumerate(self._skip_lines):
|
||||||
|
try:
|
||||||
|
self._skip_lines[idx] = re.compile(regex)
|
||||||
|
except re.error as exc:
|
||||||
|
msg = "The regex '{regex}', is not valid. The error was {err}.".format(
|
||||||
|
regex=regex, err=str(exc)
|
||||||
|
)
|
||||||
|
self._errors.append(msg)
|
||||||
|
|
||||||
|
def _xform(self):
|
||||||
|
if self._skip_lines:
|
||||||
|
if isinstance(self._before, str):
|
||||||
|
self._debug("'before' is a string, splitting lines")
|
||||||
|
self._before = self._before.splitlines()
|
||||||
|
if isinstance(self._after, str):
|
||||||
|
self._debug("'after' is a string, splitting lines")
|
||||||
|
self._after = self._after.splitlines()
|
||||||
|
self._before = [
|
||||||
|
l
|
||||||
|
for l in self._before
|
||||||
|
if not any(regex.match(str(l)) for regex in self._skip_lines)
|
||||||
|
]
|
||||||
|
self._after = [
|
||||||
|
l
|
||||||
|
for l in self._after
|
||||||
|
if not any(regex.match(str(l)) for regex in self._skip_lines)
|
||||||
|
]
|
||||||
|
if isinstance(self._before, list):
|
||||||
|
self._debug("'before' is a list, joining with \n")
|
||||||
|
self._before = "\n".join(map(str, self._before)) + "\n"
|
||||||
|
if isinstance(self._after, list):
|
||||||
|
self._debug("'after' is a list, joining with \n")
|
||||||
|
self._after = "\n".join(map(str, self._after)) + "\n"
|
||||||
|
|
||||||
|
def diff(self):
|
||||||
|
self._after = self._task_args["after"]
|
||||||
|
self._before = self._task_args["before"]
|
||||||
|
self._errors = []
|
||||||
|
self._skip_lines = self._task_args["plugin"]["vars"].get("skip_lines")
|
||||||
|
self._check_valid_regexes()
|
||||||
|
if self._errors:
|
||||||
|
return {"errors": " ".join(self._errors)}
|
||||||
|
self._xform()
|
||||||
|
diff = CallbackBase()._get_diff(
|
||||||
|
{"before": self._before, "after": self._after}
|
||||||
|
)
|
||||||
|
return {"diff": diff}
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyafter 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class FactDiffBase:
|
||||||
|
def __init__(self, task_args, task_vars, debug):
|
||||||
|
self._debug = debug
|
||||||
|
self._task_args = task_args
|
||||||
|
self._task_vars = task_vars
|
|
@ -0,0 +1,205 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
---
|
||||||
|
module: fact_diff
|
||||||
|
short_description: Find the difference between currently set facts
|
||||||
|
version_added: "1.0.0"
|
||||||
|
description:
|
||||||
|
- Compare two facts or variables and get a diff
|
||||||
|
options:
|
||||||
|
before:
|
||||||
|
description:
|
||||||
|
- The first fact to be used in the comparison
|
||||||
|
type: raw
|
||||||
|
required: True
|
||||||
|
after:
|
||||||
|
description:
|
||||||
|
- The second fact to be used in the comparison
|
||||||
|
type: raw
|
||||||
|
required: True
|
||||||
|
plugin:
|
||||||
|
description:
|
||||||
|
- Configure and specify the diff plugin to use
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
suboptions:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The diff plugin to use, in collection format
|
||||||
|
default: ansible.utils.native
|
||||||
|
type: str
|
||||||
|
vars:
|
||||||
|
description:
|
||||||
|
- Parameters passed to the diff plugin
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
suboptions:
|
||||||
|
skip_lines:
|
||||||
|
description:
|
||||||
|
- Skip lines matching these regular expressions
|
||||||
|
- Matches will be removed prior to the diff
|
||||||
|
- If the provided I(before) and I(after) are a string, they will be split
|
||||||
|
- Each entry in each list will be cast to a string for the comparison
|
||||||
|
type: list
|
||||||
|
|
||||||
|
notes:
|
||||||
|
|
||||||
|
author:
|
||||||
|
- Bradley Thornton (@cidrblock)
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- set_fact:
|
||||||
|
before:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
after:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
|
||||||
|
- name: Show the difference in json format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before }}"
|
||||||
|
after: "{{ after }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3,8 +3,8 @@
|
||||||
|
# "b": {
|
||||||
|
# "c": {
|
||||||
|
# "d": [
|
||||||
|
# - 0,
|
||||||
|
# - 1
|
||||||
|
# + 2,
|
||||||
|
# + 3
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in path format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|ansible.utils.to_paths }}"
|
||||||
|
after: "{{ after|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -1,4 +1,4 @@
|
||||||
|
# {
|
||||||
|
# - "a.b.c.d[0]": 0,
|
||||||
|
# - "a.b.c.d[1]": 1
|
||||||
|
# + "a.b.c.d[0]": 2,
|
||||||
|
# + "a.b.c.d[1]": 3
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in yaml format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|to_nice_yaml }}"
|
||||||
|
after: "{{ after|to_nice_yaml }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -2,5 +2,5 @@
|
||||||
|
# b:
|
||||||
|
# c:
|
||||||
|
# d:
|
||||||
|
# - - 0
|
||||||
|
# - - 1
|
||||||
|
# + - 2
|
||||||
|
# + - 3
|
||||||
|
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
|
||||||
|
#### Show the difference between complex object using restconf
|
||||||
|
# ansible_connection: ansible.netcommon.httpapi
|
||||||
|
# ansible_httpapi_use_ssl: True
|
||||||
|
# ansible_httpapi_validate_certs: False
|
||||||
|
# ansible_network_os: ansible.netcommon.restconf
|
||||||
|
|
||||||
|
- name: Get the current interface config prior to changes
|
||||||
|
ansible.netcommon.restconf_get:
|
||||||
|
content: config
|
||||||
|
path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
register: pre
|
||||||
|
|
||||||
|
- name: Update the description of eth1/100
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: "pre['response']['phys-items']['PhysIf-list'][{{ index }}]['descr']"
|
||||||
|
value: "Configured by ansible {{ 100 | random }}"
|
||||||
|
vars:
|
||||||
|
index: "{{ pre['response']['phys-items']['PhysIf-list']|ansible.utils.index_of('eq', 'eth1/100', 'id') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Apply the configuration
|
||||||
|
ansible.netcommon.restconf_config:
|
||||||
|
path: 'data/Cisco-NX-OS-device:System/intf-items/'
|
||||||
|
content: "{{ updated.pre.response}}"
|
||||||
|
method: patch
|
||||||
|
|
||||||
|
- name: Get the current interface config after changes
|
||||||
|
ansible.netcommon.restconf_get:
|
||||||
|
content: config
|
||||||
|
path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
register: post
|
||||||
|
|
||||||
|
- name: Show the difference
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ pre.response|ansible.utils.to_paths }}"
|
||||||
|
after: "{{ post.response|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] *********************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3604,7 +3604,7 @@
|
||||||
|
# "phys-items['PhysIf-list'][37].bw": "0",
|
||||||
|
# "phys-items['PhysIf-list'][37].controllerId": "",
|
||||||
|
# "phys-items['PhysIf-list'][37].delay": "1",
|
||||||
|
# - "phys-items['PhysIf-list'][37].descr": "Configured by ansible 95",
|
||||||
|
# + "phys-items['PhysIf-list'][37].descr": "Configured by ansible 20",
|
||||||
|
# "phys-items['PhysIf-list'][37].dot1qEtherType": "0x8100",
|
||||||
|
# "phys-items['PhysIf-list'][37].duplex": "auto",
|
||||||
|
# "phys-items['PhysIf-list'][37].id": "eth1/100",
|
||||||
|
|
||||||
|
# changed: [nxos101]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
RETURN = """
|
||||||
|
|
||||||
|
diff_text:
|
||||||
|
description: The diff in text format
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
diff_lines:
|
||||||
|
description: The C(diff_text) split into lines
|
||||||
|
returned: always
|
||||||
|
type: list
|
||||||
|
|
||||||
|
"""
|
|
@ -0,0 +1,27 @@
|
||||||
|
- name: Check argspec validation
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
ignore_errors: True
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ string in result.msg }}"
|
||||||
|
loop:
|
||||||
|
- "missing required arguments:"
|
||||||
|
- before
|
||||||
|
- after
|
||||||
|
loop_control:
|
||||||
|
loop_var: string
|
||||||
|
|
||||||
|
- name: Check argspec validation, skip_lines must be a dict
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: hostvars[inventory_hostname]
|
||||||
|
after: hostvars[inventory_hostname]
|
||||||
|
plugin:
|
||||||
|
vars:
|
||||||
|
skip_lines:
|
||||||
|
a_dict: False
|
||||||
|
ignore_errors: True
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ 'unable to convert to list' in result.msg }}"
|
|
@ -0,0 +1,128 @@
|
||||||
|
- set_fact:
|
||||||
|
before:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
after:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
d:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
|
||||||
|
- name: Show the difference in json format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before }}"
|
||||||
|
after: "{{ after }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3,8 +3,8 @@
|
||||||
|
# "b": {
|
||||||
|
# "c": {
|
||||||
|
# "d": [
|
||||||
|
# - 0,
|
||||||
|
# - 1
|
||||||
|
# + 2,
|
||||||
|
# + 3
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in path format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|ansible.utils.to_paths }}"
|
||||||
|
after: "{{ after|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -1,4 +1,4 @@
|
||||||
|
# {
|
||||||
|
# - "a.b.c.d[0]": 0,
|
||||||
|
# - "a.b.c.d[1]": 1
|
||||||
|
# + "a.b.c.d[0]": 2,
|
||||||
|
# + "a.b.c.d[1]": 3
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
- name: Show the difference in yaml format
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: "{{ before|to_nice_yaml }}"
|
||||||
|
after: "{{ after|to_nice_yaml }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] **************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -2,5 +2,5 @@
|
||||||
|
# b:
|
||||||
|
# c:
|
||||||
|
# d:
|
||||||
|
# - - 0
|
||||||
|
# - - 1
|
||||||
|
# + - 2
|
||||||
|
# + - 3
|
||||||
|
|
||||||
|
# changed: [localhost]
|
||||||
|
|
||||||
|
|
||||||
|
#### Show the difference between complex object using restconf
|
||||||
|
# ansible_connection: ansible.netcommon.httpapi
|
||||||
|
# ansible_httpapi_use_ssl: True
|
||||||
|
# ansible_httpapi_validate_certs: False
|
||||||
|
# ansible_network_os: ansible.netcommon.restconf
|
||||||
|
|
||||||
|
# - name: Get the current interface config prior to changes
|
||||||
|
# ansible.netcommon.restconf_get:
|
||||||
|
# content: config
|
||||||
|
# path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
# register: pre
|
||||||
|
|
||||||
|
# - name: Update the description of eth1/100
|
||||||
|
# ansible.utils.update_fact:
|
||||||
|
# updates:
|
||||||
|
# - path: "pre['response']['phys-items']['PhysIf-list'][{{ index }}]['descr']"
|
||||||
|
# value: "Configured by ansible {{ 100 | random }}"
|
||||||
|
# vars:
|
||||||
|
# index: "{{ pre['response']['phys-items']['PhysIf-list']|ansible.utils.index_of('eq', 'eth1/100', 'id') }}"
|
||||||
|
# register: updated
|
||||||
|
|
||||||
|
# - name: Apply the configuration
|
||||||
|
# ansible.netcommon.restconf_config:
|
||||||
|
# path: 'data/Cisco-NX-OS-device:System/intf-items/'
|
||||||
|
# content: "{{ updated.pre.response}}"
|
||||||
|
# method: patch
|
||||||
|
|
||||||
|
# - name: Get the current interface config after changes
|
||||||
|
# ansible.netcommon.restconf_get:
|
||||||
|
# content: config
|
||||||
|
# path: /data/Cisco-NX-OS-device:System/intf-items/phys-items
|
||||||
|
# register: post
|
||||||
|
|
||||||
|
# - name: Show the difference
|
||||||
|
# ansible.utils.fact_diff:
|
||||||
|
# before: "{{ pre.response|ansible.utils.to_paths }}"
|
||||||
|
# after: "{{ post.response|ansible.utils.to_paths }}"
|
||||||
|
|
||||||
|
# TASK [ansible.utils.fact_diff] *********************************************
|
||||||
|
# --- before
|
||||||
|
# +++ after
|
||||||
|
# @@ -3604,7 +3604,7 @@
|
||||||
|
# "phys-items['PhysIf-list'][37].bw": "0",
|
||||||
|
# "phys-items['PhysIf-list'][37].controllerId": "",
|
||||||
|
# "phys-items['PhysIf-list'][37].delay": "1",
|
||||||
|
# - "phys-items['PhysIf-list'][37].descr": "Configured by ansible 95",
|
||||||
|
# + "phys-items['PhysIf-list'][37].descr": "Configured by ansible 20",
|
||||||
|
# "phys-items['PhysIf-list'][37].dot1qEtherType": "0x8100",
|
||||||
|
# "phys-items['PhysIf-list'][37].duplex": "auto",
|
||||||
|
# "phys-items['PhysIf-list'][37].id": "eth1/100",
|
||||||
|
|
||||||
|
# changed: [nxos101]
|
|
@ -0,0 +1,15 @@
|
||||||
|
- name: Check for graceful fail of invalid regex
|
||||||
|
ansible.utils.fact_diff:
|
||||||
|
before: [0, 1, 2]
|
||||||
|
after: [0, 1, 2, 3]
|
||||||
|
plugin:
|
||||||
|
vars:
|
||||||
|
skip_lines:
|
||||||
|
- '+'
|
||||||
|
ignore_errors: True
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ msg in result.msg }}"
|
||||||
|
vars:
|
||||||
|
msg: "The regex '+', is not valid"
|
|
@ -0,0 +1,12 @@
|
||||||
|
- name: Recursively find all test files
|
||||||
|
find:
|
||||||
|
file_type: file
|
||||||
|
paths: "{{ role_path }}/tasks/include"
|
||||||
|
recurse: yes
|
||||||
|
use_regex: yes
|
||||||
|
patterns:
|
||||||
|
- '^(?!_).+$'
|
||||||
|
register: found
|
||||||
|
|
||||||
|
- include: "{{ item.path }}"
|
||||||
|
loop: "{{ found.files }}"
|
|
@ -0,0 +1,212 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyafter 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from mock import MagicMock
|
||||||
|
from ansible.playbook.task import Task
|
||||||
|
from ansible.template import Templar
|
||||||
|
|
||||||
|
from ansible_collections.ansible.utils.plugins.action.fact_diff import (
|
||||||
|
ActionModule,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdate_Fact(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
task = MagicMock(Task)
|
||||||
|
play_context = MagicMock()
|
||||||
|
play_context.check_mode = False
|
||||||
|
connection = MagicMock()
|
||||||
|
fake_loader = {}
|
||||||
|
templar = Templar(loader=fake_loader)
|
||||||
|
self._plugin = ActionModule(
|
||||||
|
task=task,
|
||||||
|
connection=connection,
|
||||||
|
play_context=play_context,
|
||||||
|
loader=fake_loader,
|
||||||
|
templar=templar,
|
||||||
|
shared_loader_obj=None,
|
||||||
|
)
|
||||||
|
self._plugin._task.action = "fact_diff"
|
||||||
|
self._task_vars = {"inventory_hostname": "mockdevice"}
|
||||||
|
|
||||||
|
def test_argspec_no_updates(self):
|
||||||
|
"""Check passing invalid argspec"""
|
||||||
|
self._plugin._task.args = {"before": True}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["failed"])
|
||||||
|
self.assertIn(
|
||||||
|
"missing required arguments: after",
|
||||||
|
result["msg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_same(self):
|
||||||
|
"""Ensure two equal string don't create a diff"""
|
||||||
|
before = "Lorem ipsum dolor sit amet"
|
||||||
|
after = before
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_string(self):
|
||||||
|
"""Compare two strings"""
|
||||||
|
before = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
|
||||||
|
after = "Lorem ipsum dolor sit amet, AAA consectetur adipiscing elit"
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["changed"])
|
||||||
|
self.assertIn("-" + before, result["diff_lines"])
|
||||||
|
self.assertIn("-" + before, result["diff_text"])
|
||||||
|
self.assertIn("+" + after, result["diff_lines"])
|
||||||
|
self.assertIn("+" + after, result["diff_text"])
|
||||||
|
|
||||||
|
def test_string_skip_lines(self):
|
||||||
|
"""Compare two string, with skip_lines"""
|
||||||
|
before = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
|
||||||
|
after = "Lorem ipsum dolor sit amet, AAA consectetur adipiscing elit"
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": before,
|
||||||
|
"after": after,
|
||||||
|
"plugin": {"vars": {"skip_lines": "^Lorem"}},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_same_list(self):
|
||||||
|
"""Compare two lists that are the same"""
|
||||||
|
before = [0, 1, 2, 3]
|
||||||
|
after = before
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_diff_list_skip_lines(self):
|
||||||
|
"""Compare two lists, with skip_lines"""
|
||||||
|
before = [0, 1, 2]
|
||||||
|
after = [0, 1, 2, 3]
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": before,
|
||||||
|
"after": after,
|
||||||
|
"plugin": {"vars": {"skip_lines": "3"}},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_diff_list(self):
|
||||||
|
"""Compare two lists with differences"""
|
||||||
|
before = [0, 1, 2, 3]
|
||||||
|
after = [0, 1, 2, 4]
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["changed"])
|
||||||
|
self.assertIn("-3", result["diff_lines"])
|
||||||
|
self.assertIn("-3", result["diff_text"])
|
||||||
|
self.assertIn("+4", result["diff_lines"])
|
||||||
|
self.assertIn("+4", result["diff_text"])
|
||||||
|
|
||||||
|
def test_same_dict(self):
|
||||||
|
"""Compare two dicts that are the same"""
|
||||||
|
before = {"a": {"b": {"c": {"d": [0, 1, 2]}}}}
|
||||||
|
after = before
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_diff_dict_skip_lines(self):
|
||||||
|
"""Compare two dicts, with skip_lines"""
|
||||||
|
before = {"a": {"b": {"c": {"d": [0, 1, 2]}}}}
|
||||||
|
after = {"a": {"b": {"c": {"d": [0, 1, 2, 3]}}}}
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": before,
|
||||||
|
"after": after,
|
||||||
|
"plugin": {"vars": {"skip_lines": "3"}},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertEqual([], result["diff_lines"])
|
||||||
|
self.assertEqual("", result["diff_text"])
|
||||||
|
|
||||||
|
def test_diff_dict(self):
|
||||||
|
"""Compare two dicts that are different"""
|
||||||
|
before = {"a": {"b": {"c": {"d": [0, 1, 2, 3]}}}}
|
||||||
|
after = {"a": {"b": {"c": {"d": [0, 1, 2, 4]}}}}
|
||||||
|
self._plugin._task.args = {"before": before, "after": after}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["changed"])
|
||||||
|
mlines = [l for l in result["diff_lines"] if re.match(r"^-\s+3$", l)]
|
||||||
|
self.assertEqual(1, len(mlines))
|
||||||
|
mlines = [l for l in result["diff_lines"] if re.match(r"^\+\s+4$", l)]
|
||||||
|
self.assertEqual(1, len(mlines))
|
||||||
|
|
||||||
|
def test_invalid_diff_engine_not_collection(self):
|
||||||
|
"""Check passing invalid argspec"""
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": True,
|
||||||
|
"after": True,
|
||||||
|
"plugin": {"name": "a"},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["failed"])
|
||||||
|
self.assertIn(
|
||||||
|
"Plugin name should be provided as a full name including collection",
|
||||||
|
result["msg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalid_diff_engine_not_valid(self):
|
||||||
|
"""Check passing invalid argspec"""
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": True,
|
||||||
|
"after": True,
|
||||||
|
"plugin": {"name": "a.b.c"},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["failed"])
|
||||||
|
self.assertIn(
|
||||||
|
"Error loading plugin 'a.b.c'",
|
||||||
|
result["msg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalid_regex(self):
|
||||||
|
"""Check with invalid regex"""
|
||||||
|
before = True
|
||||||
|
after = False
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"before": before,
|
||||||
|
"after": after,
|
||||||
|
"plugin": {"vars": {"skip_lines": "+"}},
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=self._task_vars)
|
||||||
|
self.assertTrue(result["failed"])
|
||||||
|
self.assertIn(
|
||||||
|
"The regex '+', is not valid",
|
||||||
|
result["msg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fail_plugin(self):
|
||||||
|
"""Simulate a diff plugin failure"""
|
||||||
|
self._plugin._result = {}
|
||||||
|
result = self._plugin._run_diff(None)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertTrue(self._plugin._result["failed"])
|
||||||
|
self.assertIn(
|
||||||
|
"'NoneType' object has no attribute 'diff'",
|
||||||
|
self._plugin._result["msg"],
|
||||||
|
)
|
|
@ -1,5 +1,7 @@
|
||||||
# (c) 2020 Ansible Project
|
# -*- coding: utf-8 -*-
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# Copyafter 2020 Red Hat
|
||||||
|
# GNU General Public License v3.0+
|
||||||
|
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue