update_fact plugin + units (#10)
* update_fact plugin + units * Update README, add doc * Add some update_fact integration tests * Add missing test for not (index_of) * Update fact doc updates * Update rst from doc * Examples as yaml * Some developer notes (#11) Co-authored-by: cidrblock <brad@thethorntons.net> * Updated self._task.args from updated_data provided back from aav * Argspec default fix (#12) * Return data updated with default values from aav.validate() * Update aav docs Co-authored-by: cidrblock <brad@thethorntons.net> * update_fact plugin + units * Update README, add doc * Add some update_fact integration tests * Add missing test for not (index_of) * Update fact doc updates * Update rst from doc * Examples as yaml * Updated self._task.args from updated_data provided back from aav * recheck * Plugin cleanup (#14) * WIP * Add argspec validation to plugins, restructure tests * Update docs * Pre PY3.8 compat changes * Run black to fix quotes * Seems the order of missing keys varies between 2.9 and 2.10 * More error ordering issues fixed during argspec validation * More black, wrong version Co-authored-by: cidrblock <brad@thethorntons.net> * update_fact plugin + units * Update README, add doc * Add some update_fact integration tests * Update fact doc updates * Update rst from doc * Examples as yaml * Updated self._task.args from updated_data provided back from aav * recheck Co-authored-by: cidrblock <brad@thethorntons.net>pull/16/head
parent
bcddde229d
commit
188463f7be
|
@ -32,6 +32,11 @@ Name | Description
|
||||||
[ansible.utils.index_of](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.index_of_lookup.rst)|Find the indicies of items in a list matching some criteria
|
[ansible.utils.index_of](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.index_of_lookup.rst)|Find the indicies of items in a list matching some criteria
|
||||||
[ansible.utils.to_paths](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst)|Flatten a complex object into a dictionary of paths and values
|
[ansible.utils.to_paths](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst)|Flatten a complex object into a dictionary of paths and values
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
Name | Description
|
||||||
|
--- | ---
|
||||||
|
[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-->
|
||||||
|
|
||||||
## Installing this collection
|
## Installing this collection
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
.. _ansible.utils.update_fact_module:
|
||||||
|
|
||||||
|
|
||||||
|
*************************
|
||||||
|
ansible.utils.update_fact
|
||||||
|
*************************
|
||||||
|
|
||||||
|
**Update currently set facts**
|
||||||
|
|
||||||
|
|
||||||
|
Version added: 1.0.0
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
:depth: 1
|
||||||
|
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
- This module allows updating existing variables.
|
||||||
|
- Variables are updated on a host-by-host basis.
|
||||||
|
- Variable are not modified in place, instead they are returned by the module
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<table border=0 cellpadding=0 class="documentation-table">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Parameter</th>
|
||||||
|
<th>Choices/<font color="blue">Defaults</font></th>
|
||||||
|
<th width="100%">Comments</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>updates</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">list</span>
|
||||||
|
/ <span style="color: purple">elements=dictionary</span>
|
||||||
|
/ <span style="color: red">required</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>A list of dictionaries, each a desired update to make</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td colspan="1">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>path</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">string</span>
|
||||||
|
/ <span style="color: red">required</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>The path in a currently set variable to update</div>
|
||||||
|
<div>The path can be in dot or bracket notation</div>
|
||||||
|
<div>It should be a valid jinja reference</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="elbow-placeholder"></td>
|
||||||
|
<td colspan="1">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>value</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 value to be set at the path</div>
|
||||||
|
<div>Can be a simple or complex data structure</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
# Update an exisitng fact, dot or bracket notation
|
||||||
|
- name: Set a fact
|
||||||
|
set_fact:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
- name: Update the fact
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a.b.c.0
|
||||||
|
value: 10
|
||||||
|
- path: "a['b']['c'][1]"
|
||||||
|
value: 20
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated.a
|
||||||
|
|
||||||
|
# updated:
|
||||||
|
# a:
|
||||||
|
# b:
|
||||||
|
# c:
|
||||||
|
# - 10
|
||||||
|
# - 20
|
||||||
|
# changed: true
|
||||||
|
|
||||||
|
|
||||||
|
# Lists can be appended, new keys added to dictionaries
|
||||||
|
|
||||||
|
- name: Set a fact
|
||||||
|
set_fact:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
b1:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
- name: Update, add to list, add new key
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a.b.b1.2
|
||||||
|
value: 3
|
||||||
|
- path: a.b.b2
|
||||||
|
value:
|
||||||
|
- 10
|
||||||
|
- 20
|
||||||
|
- 30
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated.a
|
||||||
|
|
||||||
|
# updated:
|
||||||
|
# a:
|
||||||
|
# b:
|
||||||
|
# b1:
|
||||||
|
# - 1
|
||||||
|
# - 2
|
||||||
|
# - 3
|
||||||
|
# b2:
|
||||||
|
# - 10
|
||||||
|
# - 20
|
||||||
|
# - 30
|
||||||
|
# changed: true
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Update every item in a list of dictionaries
|
||||||
|
# build the update list ahead of time using a loop
|
||||||
|
# and then apply the changes to the fact
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Set fact
|
||||||
|
set_fact:
|
||||||
|
addresses:
|
||||||
|
- raw: 10.1.1.0/255.255.255.0
|
||||||
|
name: servers
|
||||||
|
- raw: 192.168.1.0/255.255.255.0
|
||||||
|
name: printers
|
||||||
|
- raw: 8.8.8.8
|
||||||
|
name: dns
|
||||||
|
|
||||||
|
- name: Build a list of updates
|
||||||
|
set_fact:
|
||||||
|
update_list: "{{ update_list + update }}"
|
||||||
|
loop: "{{ addresses }}"
|
||||||
|
loop_control:
|
||||||
|
index_var: idx
|
||||||
|
vars:
|
||||||
|
update_list: []
|
||||||
|
update:
|
||||||
|
- path: addresses[{{ idx }}].network
|
||||||
|
value: "{{ item['raw'] | ansible.netcommon.ipaddr('network') }}"
|
||||||
|
- path: addresses[{{ idx }}].prefix
|
||||||
|
value: "{{ item['raw'] | ansible.netcommon.ipaddr('prefix') }}"
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: update_list
|
||||||
|
|
||||||
|
# TASK [debug] *******************
|
||||||
|
# ok: [localhost] =>
|
||||||
|
# update_list:
|
||||||
|
# - path: addresses[0].network
|
||||||
|
# value: 10.1.1.0
|
||||||
|
# - path: addresses[0].prefix
|
||||||
|
# value: '24'
|
||||||
|
# - path: addresses[1].network
|
||||||
|
# value: 192.168.1.0
|
||||||
|
# - path: addresses[1].prefix
|
||||||
|
# value: '24'
|
||||||
|
# - path: addresses[2].network
|
||||||
|
# value: 8.8.8.8
|
||||||
|
# - path: addresses[2].prefix
|
||||||
|
# value: '32'
|
||||||
|
|
||||||
|
- name: Make the updates
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates: "{{ update_list }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated
|
||||||
|
|
||||||
|
# TASK [debug] ***********************
|
||||||
|
# ok: [localhost] =>
|
||||||
|
# updated:
|
||||||
|
# addresses:
|
||||||
|
# - name: servers
|
||||||
|
# network: 10.1.1.0
|
||||||
|
# prefix: '24'
|
||||||
|
# raw: 10.1.1.0/255.255.255.0
|
||||||
|
# - name: printers
|
||||||
|
# network: 192.168.1.0
|
||||||
|
# prefix: '24'
|
||||||
|
# raw: 192.168.1.0/255.255.255.0
|
||||||
|
# - name: dns
|
||||||
|
# network: 8.8.8.8
|
||||||
|
# prefix: '32'
|
||||||
|
# raw: 8.8.8.8
|
||||||
|
# changed: true
|
||||||
|
# failed: false
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Retrieve, update, and apply interface description change
|
||||||
|
# use index_of to locate Etherent1/1
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Get the current interface config
|
||||||
|
cisco.nxos.nxos_interfaces:
|
||||||
|
state: gathered
|
||||||
|
register: interfaces
|
||||||
|
|
||||||
|
- name: Update the description of Ethernet1/1
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: "interfaces.gathered[{{ index }}].description"
|
||||||
|
value: "Configured by ansible"
|
||||||
|
vars:
|
||||||
|
index: "{{ interfaces.gathered|ansible.utils.index_of('eq', 'Ethernet1/1', 'name') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Update the configuration
|
||||||
|
cisco.nxos.nxos_interfaces:
|
||||||
|
config: "{{ updated.interfaces.gathered }}"
|
||||||
|
state: overridden
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Show the commands issued
|
||||||
|
debug:
|
||||||
|
msg: "{{ result['commands'] }}"
|
||||||
|
|
||||||
|
# TASK [Show the commands issued] *************************************
|
||||||
|
# ok: [nxos101] => {
|
||||||
|
# "msg": [
|
||||||
|
# "interface Ethernet1/1",
|
||||||
|
# "description Configured by ansible"
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Retrieve, update, and apply an ipv4 ACL change
|
||||||
|
# finding the index of AFI ipv4 acls
|
||||||
|
# finding the index of the ACL named 'test1'
|
||||||
|
# finding the index of sequence 10
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Retrieve the current acls
|
||||||
|
arista.eos.eos_acls:
|
||||||
|
state: gathered
|
||||||
|
register: current
|
||||||
|
|
||||||
|
- name: Update the source of sequenmce 10 in the IPv4 ACL named test1
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: current.gathered[{{ afi }}].acls[{{ acl }}].aces[{{ ace }}].source
|
||||||
|
value:
|
||||||
|
subnet_address: "192.168.2.0/24"
|
||||||
|
vars:
|
||||||
|
afi: "{{ current.gathered|ansible.utils.index_of('eq', 'ipv4', 'afi') }}"
|
||||||
|
acl: "{{ current.gathered[afi|int].acls|ansible.utils.index_of('eq', 'test1', 'name') }}"
|
||||||
|
ace: "{{ current.gathered[afi|int].acls[acl|int].aces|ansible.utils.index_of('eq', 10, 'sequence') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Apply the changes
|
||||||
|
arista.eos.eos_acls:
|
||||||
|
config: "{{ updated.current.gathered }}"
|
||||||
|
state: overridden
|
||||||
|
register: changes
|
||||||
|
|
||||||
|
- name: Show the commands issued
|
||||||
|
debug:
|
||||||
|
msg: "{{ changes['commands'] }}"
|
||||||
|
|
||||||
|
# TASK [Show the commands issued] *************************************
|
||||||
|
# ok: [eos101] => {
|
||||||
|
# "msg": [
|
||||||
|
# "ip access-list test1",
|
||||||
|
# "no 10",
|
||||||
|
# "10 permit ip 192.168.2.0/24 host 10.1.1.2"
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Disable ip redirects on any layer3 interface
|
||||||
|
# find the layer 3 interfaces
|
||||||
|
# use each name to find their index in l3 interface
|
||||||
|
# build an 'update' list and apply the updates
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Get the current interface and L3 interface configuration
|
||||||
|
cisco.nxos.nxos_facts:
|
||||||
|
gather_subset: min
|
||||||
|
gather_network_resources:
|
||||||
|
- interfaces
|
||||||
|
- l3_interfaces
|
||||||
|
|
||||||
|
- name: Build the list of updates to make
|
||||||
|
set_fact:
|
||||||
|
updates: "{{ updates + [entry] }}"
|
||||||
|
vars:
|
||||||
|
updates: []
|
||||||
|
entry:
|
||||||
|
path: "ansible_network_resources.l3_interfaces[{{ item }}].redirects"
|
||||||
|
value: False
|
||||||
|
w_mode: "{{ ansible_network_resources.interfaces|selectattr('mode', 'defined') }}"
|
||||||
|
m_l3: "{{ w_mode|selectattr('mode', 'eq', 'layer3') }}"
|
||||||
|
names: "{{ m_l3|map(attribute='name')|list }}"
|
||||||
|
l3_indicies: "{{ ansible_network_resources.l3_interfaces|ansible.utils.index_of('in', names, 'name', wantlist=True) }}"
|
||||||
|
loop: "{{ l3_indicies }}"
|
||||||
|
|
||||||
|
# TASK [Build the list of updates to make] ****************************
|
||||||
|
# ok: [nxos101] => (item=99) => changed=false
|
||||||
|
# ansible_facts:
|
||||||
|
# updates:
|
||||||
|
# - path: ansible_network_resources.l3_interfaces[99].redirects
|
||||||
|
# value: false
|
||||||
|
# ansible_loop_var: item
|
||||||
|
# item: 99
|
||||||
|
|
||||||
|
- name: Update the l3 interfaces
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates: "{{ updates }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
# TASK [Update the l3 interfaces] *************************************
|
||||||
|
# changed: [nxos101] => changed=true
|
||||||
|
# ansible_network_resources:
|
||||||
|
# l3_interfaces:
|
||||||
|
# <...>
|
||||||
|
# - ipv4:
|
||||||
|
# - address: 10.1.1.1/24
|
||||||
|
# name: Ethernet1/100
|
||||||
|
# redirects: false
|
||||||
|
|
||||||
|
- name: Apply the configuration changes
|
||||||
|
cisco.nxos.l3_interfaces:
|
||||||
|
config: "{{ updated.ansible_network_resources.l3_interfaces }}"
|
||||||
|
state: overridden
|
||||||
|
register: changes
|
||||||
|
|
||||||
|
# TASK [Apply the configuration changes] ******************************
|
||||||
|
# changed: [nxos101] => changed=true
|
||||||
|
# commands:
|
||||||
|
# - interface Ethernet1/100
|
||||||
|
# - no ip redirects
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Status
|
||||||
|
------
|
||||||
|
|
||||||
|
|
||||||
|
Authors
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
- Bradley Thornton (@cidrblock)
|
|
@ -0,0 +1,189 @@
|
||||||
|
# -*- 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
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.errors import AnsibleActionFail
|
||||||
|
|
||||||
|
from ansible.module_utils.common._collections_compat import (
|
||||||
|
MutableMapping,
|
||||||
|
MutableSequence,
|
||||||
|
)
|
||||||
|
from ansible.module_utils import basic
|
||||||
|
from ansible.module_utils._text import to_bytes, to_native
|
||||||
|
from jinja2 import Template, TemplateSyntaxError
|
||||||
|
from ansible_collections.ansible.utils.plugins.modules.update_fact import (
|
||||||
|
DOCUMENTATION,
|
||||||
|
)
|
||||||
|
from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
|
||||||
|
AnsibleArgSpecValidator,
|
||||||
|
)
|
||||||
|
from ansible.errors import AnsibleActionFail
|
||||||
|
|
||||||
|
|
||||||
|
class ActionModule(ActionBase):
|
||||||
|
"""action module"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Start here"""
|
||||||
|
super(ActionModule, self).__init__(*args, **kwargs)
|
||||||
|
self._supports_async = True
|
||||||
|
self._updates = None
|
||||||
|
self._result = 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:
|
||||||
|
raise AnsibleActionFail(errors)
|
||||||
|
|
||||||
|
def _ensure_valid_jinja(self):
|
||||||
|
"""Ensure each path is jinja valid"""
|
||||||
|
errors = []
|
||||||
|
for entry in self._task.args["updates"]:
|
||||||
|
try:
|
||||||
|
Template("{{" + entry["path"] + "}}")
|
||||||
|
except TemplateSyntaxError as exc:
|
||||||
|
error = (
|
||||||
|
"While processing '{path}' found malformed path."
|
||||||
|
" Ensure syntax follows valid jinja format. The error was:"
|
||||||
|
" {error}"
|
||||||
|
).format(path=entry["path"], error=to_native(exc))
|
||||||
|
errors.append(error)
|
||||||
|
if errors:
|
||||||
|
raise AnsibleActionFail(" ".join(errors))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _field_split(path):
|
||||||
|
"""Split the path into it's parts
|
||||||
|
|
||||||
|
:param path: The user provided path
|
||||||
|
:type path: str
|
||||||
|
:return: the individual parts of the path
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
que = list(path)
|
||||||
|
val = que.pop(0)
|
||||||
|
fields = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
field = ""
|
||||||
|
# found a '.', move to the next character
|
||||||
|
if val == ".":
|
||||||
|
val = que.pop(0)
|
||||||
|
# found a '[', pop until ']' and then get the next
|
||||||
|
if val == "[":
|
||||||
|
val = que.pop(0)
|
||||||
|
while val != "]":
|
||||||
|
field += val
|
||||||
|
val = que.pop(0)
|
||||||
|
val = que.pop(0)
|
||||||
|
else:
|
||||||
|
while val not in [".", "["]:
|
||||||
|
field += val
|
||||||
|
val = que.pop(0)
|
||||||
|
try:
|
||||||
|
# make numbers numbers
|
||||||
|
fields.append(ast.literal_eval(field))
|
||||||
|
except Exception:
|
||||||
|
# or strip the quotes
|
||||||
|
fields.append(re.sub("['\"]", "", field))
|
||||||
|
except IndexError:
|
||||||
|
# pop'ed past the end of the que
|
||||||
|
# so add the final field
|
||||||
|
try:
|
||||||
|
fields.append(ast.literal_eval(field))
|
||||||
|
except Exception:
|
||||||
|
fields.append(re.sub("['\"]", "", field))
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def set_value(self, obj, path, val):
|
||||||
|
"""Set a value
|
||||||
|
|
||||||
|
:param obj: The object to modify
|
||||||
|
:type obj: mutable object
|
||||||
|
:param path: The path to where the update should be made
|
||||||
|
:type path: list
|
||||||
|
:param val: The new value to place at path
|
||||||
|
:type val: string, dict, list, bool, etc
|
||||||
|
"""
|
||||||
|
first, rest = path[0], path[1:]
|
||||||
|
if rest:
|
||||||
|
try:
|
||||||
|
new_obj = obj[first]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
msg = (
|
||||||
|
"Error: the key '{first}' was not found "
|
||||||
|
"in {obj}.".format(obj=obj, first=first)
|
||||||
|
)
|
||||||
|
raise AnsibleActionFail(msg)
|
||||||
|
self.set_value(new_obj, rest, val)
|
||||||
|
else:
|
||||||
|
if isinstance(obj, MutableMapping):
|
||||||
|
if obj.get(first) != val:
|
||||||
|
self._result["changed"] = True
|
||||||
|
obj[first] = val
|
||||||
|
elif isinstance(obj, MutableSequence):
|
||||||
|
if not isinstance(first, int):
|
||||||
|
msg = (
|
||||||
|
"Error: {obj} is a list, "
|
||||||
|
"but index provided was not an integer: '{first}'"
|
||||||
|
).format(obj=obj, first=first)
|
||||||
|
raise AnsibleActionFail(msg)
|
||||||
|
if first > len(obj):
|
||||||
|
msg = "Error: {obj} not long enough for item #{first} to be set.".format(
|
||||||
|
obj=obj, first=first
|
||||||
|
)
|
||||||
|
raise AnsibleActionFail(msg)
|
||||||
|
if first == len(obj):
|
||||||
|
obj.append(val)
|
||||||
|
self._result["changed"] = True
|
||||||
|
else:
|
||||||
|
if obj[first] != val:
|
||||||
|
obj[first] = val
|
||||||
|
self._result["changed"] = True
|
||||||
|
else:
|
||||||
|
msg = "update_fact can only modify mutable objects."
|
||||||
|
raise AnsibleActionFail(msg)
|
||||||
|
|
||||||
|
def run(self, tmp=None, task_vars=None):
|
||||||
|
"""action entry point"""
|
||||||
|
self._task.diff = False
|
||||||
|
self._result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
|
self._result["changed"] = False
|
||||||
|
self._check_argspec()
|
||||||
|
results = set()
|
||||||
|
self._ensure_valid_jinja()
|
||||||
|
for entry in self._task.args["updates"]:
|
||||||
|
parts = self._field_split(entry["path"])
|
||||||
|
obj, path = parts[0], parts[1:]
|
||||||
|
results.add(obj)
|
||||||
|
if obj not in task_vars["vars"]:
|
||||||
|
msg = "'{obj}' was not found in the current facts.".format(
|
||||||
|
obj=obj
|
||||||
|
)
|
||||||
|
raise AnsibleActionFail(msg)
|
||||||
|
retrieved = task_vars["vars"].get(obj)
|
||||||
|
if path:
|
||||||
|
self.set_value(retrieved, path, entry["value"])
|
||||||
|
else:
|
||||||
|
if task_vars["vars"][obj] != entry["value"]:
|
||||||
|
task_vars["vars"][obj] = entry["value"]
|
||||||
|
self._result["changed"] = True
|
||||||
|
|
||||||
|
for key in results:
|
||||||
|
value = task_vars["vars"].get(key)
|
||||||
|
self._result[key] = value
|
||||||
|
return self._result
|
|
@ -0,0 +1,343 @@
|
||||||
|
# -*- 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: update_fact
|
||||||
|
short_description: Update currently set facts
|
||||||
|
version_added: "1.0.0"
|
||||||
|
description:
|
||||||
|
- This module allows updating existing variables.
|
||||||
|
- Variables are updated on a host-by-host basis.
|
||||||
|
- Variable are not modified in place, instead they are returned by the module
|
||||||
|
options:
|
||||||
|
updates:
|
||||||
|
description:
|
||||||
|
- A list of dictionaries, each a desired update to make
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
required: True
|
||||||
|
suboptions:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The path in a currently set variable to update
|
||||||
|
- The path can be in dot or bracket notation
|
||||||
|
- It should be a valid jinja reference
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
value:
|
||||||
|
description:
|
||||||
|
- The value to be set at the path
|
||||||
|
- Can be a simple or complex data structure
|
||||||
|
type: raw
|
||||||
|
required: True
|
||||||
|
|
||||||
|
|
||||||
|
notes:
|
||||||
|
|
||||||
|
author:
|
||||||
|
- Bradley Thornton (@cidrblock)
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
|
||||||
|
# Update an exisitng fact, dot or bracket notation
|
||||||
|
- name: Set a fact
|
||||||
|
set_fact:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
- name: Update the fact
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a.b.c.0
|
||||||
|
value: 10
|
||||||
|
- path: "a['b']['c'][1]"
|
||||||
|
value: 20
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated.a
|
||||||
|
|
||||||
|
# updated:
|
||||||
|
# a:
|
||||||
|
# b:
|
||||||
|
# c:
|
||||||
|
# - 10
|
||||||
|
# - 20
|
||||||
|
# changed: true
|
||||||
|
|
||||||
|
|
||||||
|
# Lists can be appended, new keys added to dictionaries
|
||||||
|
|
||||||
|
- name: Set a fact
|
||||||
|
set_fact:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
b1:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
- name: Update, add to list, add new key
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a.b.b1.2
|
||||||
|
value: 3
|
||||||
|
- path: a.b.b2
|
||||||
|
value:
|
||||||
|
- 10
|
||||||
|
- 20
|
||||||
|
- 30
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated.a
|
||||||
|
|
||||||
|
# updated:
|
||||||
|
# a:
|
||||||
|
# b:
|
||||||
|
# b1:
|
||||||
|
# - 1
|
||||||
|
# - 2
|
||||||
|
# - 3
|
||||||
|
# b2:
|
||||||
|
# - 10
|
||||||
|
# - 20
|
||||||
|
# - 30
|
||||||
|
# changed: true
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Update every item in a list of dictionaries
|
||||||
|
# build the update list ahead of time using a loop
|
||||||
|
# and then apply the changes to the fact
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Set fact
|
||||||
|
set_fact:
|
||||||
|
addresses:
|
||||||
|
- raw: 10.1.1.0/255.255.255.0
|
||||||
|
name: servers
|
||||||
|
- raw: 192.168.1.0/255.255.255.0
|
||||||
|
name: printers
|
||||||
|
- raw: 8.8.8.8
|
||||||
|
name: dns
|
||||||
|
|
||||||
|
- name: Build a list of updates
|
||||||
|
set_fact:
|
||||||
|
update_list: "{{ update_list + update }}"
|
||||||
|
loop: "{{ addresses }}"
|
||||||
|
loop_control:
|
||||||
|
index_var: idx
|
||||||
|
vars:
|
||||||
|
update_list: []
|
||||||
|
update:
|
||||||
|
- path: addresses[{{ idx }}].network
|
||||||
|
value: "{{ item['raw'] | ansible.netcommon.ipaddr('network') }}"
|
||||||
|
- path: addresses[{{ idx }}].prefix
|
||||||
|
value: "{{ item['raw'] | ansible.netcommon.ipaddr('prefix') }}"
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: update_list
|
||||||
|
|
||||||
|
# TASK [debug] *******************
|
||||||
|
# ok: [localhost] =>
|
||||||
|
# update_list:
|
||||||
|
# - path: addresses[0].network
|
||||||
|
# value: 10.1.1.0
|
||||||
|
# - path: addresses[0].prefix
|
||||||
|
# value: '24'
|
||||||
|
# - path: addresses[1].network
|
||||||
|
# value: 192.168.1.0
|
||||||
|
# - path: addresses[1].prefix
|
||||||
|
# value: '24'
|
||||||
|
# - path: addresses[2].network
|
||||||
|
# value: 8.8.8.8
|
||||||
|
# - path: addresses[2].prefix
|
||||||
|
# value: '32'
|
||||||
|
|
||||||
|
- name: Make the updates
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates: "{{ update_list }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: updated
|
||||||
|
|
||||||
|
# TASK [debug] ***********************
|
||||||
|
# ok: [localhost] =>
|
||||||
|
# updated:
|
||||||
|
# addresses:
|
||||||
|
# - name: servers
|
||||||
|
# network: 10.1.1.0
|
||||||
|
# prefix: '24'
|
||||||
|
# raw: 10.1.1.0/255.255.255.0
|
||||||
|
# - name: printers
|
||||||
|
# network: 192.168.1.0
|
||||||
|
# prefix: '24'
|
||||||
|
# raw: 192.168.1.0/255.255.255.0
|
||||||
|
# - name: dns
|
||||||
|
# network: 8.8.8.8
|
||||||
|
# prefix: '32'
|
||||||
|
# raw: 8.8.8.8
|
||||||
|
# changed: true
|
||||||
|
# failed: false
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Retrieve, update, and apply interface description change
|
||||||
|
# use index_of to locate Etherent1/1
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Get the current interface config
|
||||||
|
cisco.nxos.nxos_interfaces:
|
||||||
|
state: gathered
|
||||||
|
register: interfaces
|
||||||
|
|
||||||
|
- name: Update the description of Ethernet1/1
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: "interfaces.gathered[{{ index }}].description"
|
||||||
|
value: "Configured by ansible"
|
||||||
|
vars:
|
||||||
|
index: "{{ interfaces.gathered|ansible.utils.index_of('eq', 'Ethernet1/1', 'name') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Update the configuration
|
||||||
|
cisco.nxos.nxos_interfaces:
|
||||||
|
config: "{{ updated.interfaces.gathered }}"
|
||||||
|
state: overridden
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Show the commands issued
|
||||||
|
debug:
|
||||||
|
msg: "{{ result['commands'] }}"
|
||||||
|
|
||||||
|
# TASK [Show the commands issued] *************************************
|
||||||
|
# ok: [nxos101] => {
|
||||||
|
# "msg": [
|
||||||
|
# "interface Ethernet1/1",
|
||||||
|
# "description Configured by ansible"
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Retrieve, update, and apply an ipv4 ACL change
|
||||||
|
# finding the index of AFI ipv4 acls
|
||||||
|
# finding the index of the ACL named 'test1'
|
||||||
|
# finding the index of sequence 10
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Retrieve the current acls
|
||||||
|
arista.eos.eos_acls:
|
||||||
|
state: gathered
|
||||||
|
register: current
|
||||||
|
|
||||||
|
- name: Update the source of sequenmce 10 in the IPv4 ACL named test1
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: current.gathered[{{ afi }}].acls[{{ acl }}].aces[{{ ace }}].source
|
||||||
|
value:
|
||||||
|
subnet_address: "192.168.2.0/24"
|
||||||
|
vars:
|
||||||
|
afi: "{{ current.gathered|ansible.utils.index_of('eq', 'ipv4', 'afi') }}"
|
||||||
|
acl: "{{ current.gathered[afi|int].acls|ansible.utils.index_of('eq', 'test1', 'name') }}"
|
||||||
|
ace: "{{ current.gathered[afi|int].acls[acl|int].aces|ansible.utils.index_of('eq', 10, 'sequence') }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- name: Apply the changes
|
||||||
|
arista.eos.eos_acls:
|
||||||
|
config: "{{ updated.current.gathered }}"
|
||||||
|
state: overridden
|
||||||
|
register: changes
|
||||||
|
|
||||||
|
- name: Show the commands issued
|
||||||
|
debug:
|
||||||
|
msg: "{{ changes['commands'] }}"
|
||||||
|
|
||||||
|
# TASK [Show the commands issued] *************************************
|
||||||
|
# ok: [eos101] => {
|
||||||
|
# "msg": [
|
||||||
|
# "ip access-list test1",
|
||||||
|
# "no 10",
|
||||||
|
# "10 permit ip 192.168.2.0/24 host 10.1.1.2"
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Disable ip redirects on any layer3 interface
|
||||||
|
# find the layer 3 interfaces
|
||||||
|
# use each name to find their index in l3 interface
|
||||||
|
# build an 'update' list and apply the updates
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
- name: Get the current interface and L3 interface configuration
|
||||||
|
cisco.nxos.nxos_facts:
|
||||||
|
gather_subset: min
|
||||||
|
gather_network_resources:
|
||||||
|
- interfaces
|
||||||
|
- l3_interfaces
|
||||||
|
|
||||||
|
- name: Build the list of updates to make
|
||||||
|
set_fact:
|
||||||
|
updates: "{{ updates + [entry] }}"
|
||||||
|
vars:
|
||||||
|
updates: []
|
||||||
|
entry:
|
||||||
|
path: "ansible_network_resources.l3_interfaces[{{ item }}].redirects"
|
||||||
|
value: False
|
||||||
|
w_mode: "{{ ansible_network_resources.interfaces|selectattr('mode', 'defined') }}"
|
||||||
|
m_l3: "{{ w_mode|selectattr('mode', 'eq', 'layer3') }}"
|
||||||
|
names: "{{ m_l3|map(attribute='name')|list }}"
|
||||||
|
l3_indicies: "{{ ansible_network_resources.l3_interfaces|ansible.utils.index_of('in', names, 'name', wantlist=True) }}"
|
||||||
|
loop: "{{ l3_indicies }}"
|
||||||
|
|
||||||
|
# TASK [Build the list of updates to make] ****************************
|
||||||
|
# ok: [nxos101] => (item=99) => changed=false
|
||||||
|
# ansible_facts:
|
||||||
|
# updates:
|
||||||
|
# - path: ansible_network_resources.l3_interfaces[99].redirects
|
||||||
|
# value: false
|
||||||
|
# ansible_loop_var: item
|
||||||
|
# item: 99
|
||||||
|
|
||||||
|
- name: Update the l3 interfaces
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates: "{{ updates }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
# TASK [Update the l3 interfaces] *************************************
|
||||||
|
# changed: [nxos101] => changed=true
|
||||||
|
# ansible_network_resources:
|
||||||
|
# l3_interfaces:
|
||||||
|
# <...>
|
||||||
|
# - ipv4:
|
||||||
|
# - address: 10.1.1.1/24
|
||||||
|
# name: Ethernet1/100
|
||||||
|
# redirects: false
|
||||||
|
|
||||||
|
- name: Apply the configuration changes
|
||||||
|
cisco.nxos.l3_interfaces:
|
||||||
|
config: "{{ updated.ansible_network_resources.l3_interfaces }}"
|
||||||
|
state: overridden
|
||||||
|
register: changes
|
||||||
|
|
||||||
|
# TASK [Apply the configuration changes] ******************************
|
||||||
|
# changed: [nxos101] => changed=true
|
||||||
|
# commands:
|
||||||
|
# - interface Ethernet1/100
|
||||||
|
# - no ip redirects
|
||||||
|
|
||||||
|
"""
|
|
@ -0,0 +1,68 @@
|
||||||
|
- name: Set a fact
|
||||||
|
set_fact:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
- name: Update the fact
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a.b.c.0
|
||||||
|
value: 10
|
||||||
|
- path: "a['b']['c'][1]"
|
||||||
|
value: 20
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ updated.a == expected.a }}"
|
||||||
|
vars:
|
||||||
|
expected:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
- 10
|
||||||
|
- 20
|
||||||
|
|
||||||
|
- name: Update the fact
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: a
|
||||||
|
value:
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z:
|
||||||
|
- 100
|
||||||
|
- True
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ updated.a == expected.a }}"
|
||||||
|
vars:
|
||||||
|
expected:
|
||||||
|
a:
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z:
|
||||||
|
- 100
|
||||||
|
- True
|
||||||
|
|
||||||
|
- name: Update the fact
|
||||||
|
ansible.utils.update_fact:
|
||||||
|
updates:
|
||||||
|
- path: "a.b.c[{{ index }}]"
|
||||||
|
value: 20
|
||||||
|
vars:
|
||||||
|
index: "{{ a.b.c|ansible.utils.index_of('eq', 2) }}"
|
||||||
|
register: updated
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that: "{{ updated.a == expected.a }}"
|
||||||
|
vars:
|
||||||
|
expected:
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c:
|
||||||
|
- 1
|
||||||
|
- 20
|
|
@ -0,0 +1,366 @@
|
||||||
|
# (c) 2020 Ansible Project
|
||||||
|
# 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 unittest
|
||||||
|
from jinja2 import Template, TemplateSyntaxError
|
||||||
|
from mock import MagicMock
|
||||||
|
from ansible.playbook.task import Task
|
||||||
|
from ansible.template import Templar
|
||||||
|
|
||||||
|
from ansible_collections.ansible.utils.plugins.action.update_fact import (
|
||||||
|
ActionModule,
|
||||||
|
)
|
||||||
|
|
||||||
|
VALID_DATA = {
|
||||||
|
"a": {
|
||||||
|
"b": {"4.4": [{"1": {5: {"foo": 123}}}], 5.5: "float5.5"},
|
||||||
|
"127.0.0.1": "localhost",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VALID_TESTS = [
|
||||||
|
{
|
||||||
|
"path": 'a.b["4.4"][0]["1"].5[\'foo\']',
|
||||||
|
"split": ["a", "b", "4.4", 0, "1", 5, "foo"],
|
||||||
|
"template_result": "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": 'a.b["4.4"][0]["1"].5[\'foo\']',
|
||||||
|
"split": ["a", "b", "4.4", 0, "1", 5, "foo"],
|
||||||
|
"template_result": "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "a.b[5.5]",
|
||||||
|
"split": ["a", "b", 5.5],
|
||||||
|
"template_result": "float5.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "a['127.0.0.1']",
|
||||||
|
"split": ["a", "127.0.0.1"],
|
||||||
|
"template_result": "localhost",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "a.b['4.4'].0['1'].5['foo']",
|
||||||
|
"split": ["a", "b", "4.4", 0, "1", 5, "foo"],
|
||||||
|
"template_result": "123",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
INVALID_JINJA = [
|
||||||
|
{
|
||||||
|
"path": "a.'1'",
|
||||||
|
"note": "quoted values are required to be in brackets",
|
||||||
|
"error": "expected name or number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "a.[1]",
|
||||||
|
"note": "brackets can't follow dots",
|
||||||
|
"error": "expected name or number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": 'a.b["4.4"][0]["1"]."5"[\'foo\']',
|
||||||
|
"note": "quoted values are required to be in brackets",
|
||||||
|
"error": "expected name or number",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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 = "update_fact"
|
||||||
|
|
||||||
|
def test_argspec_no_updates(self):
|
||||||
|
"""Check passing invalid argspec"""
|
||||||
|
self._plugin._task.args = {"a": 10}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=None)
|
||||||
|
self.assertIn(
|
||||||
|
"missing required arguments: updates",
|
||||||
|
str(error.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_argspec_none(self):
|
||||||
|
"""Check passing a dict"""
|
||||||
|
self._plugin._task.args = {}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=None)
|
||||||
|
self.assertIn(
|
||||||
|
"missing required arguments: updates", str(error.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_valid_jinja(self):
|
||||||
|
for test in VALID_TESTS:
|
||||||
|
tmplt = Template("{{" + test["path"] + "}}")
|
||||||
|
result = tmplt.render(VALID_DATA)
|
||||||
|
self.assertEqual(result, test["template_result"])
|
||||||
|
|
||||||
|
def test_invalid_jinja(self):
|
||||||
|
for test in INVALID_JINJA:
|
||||||
|
with self.assertRaises(TemplateSyntaxError) as error:
|
||||||
|
Template("{{" + test["path"] + "}}")
|
||||||
|
self.assertIn(test["error"], str(error.exception))
|
||||||
|
|
||||||
|
def test_fields(self):
|
||||||
|
"""Check the parsing of a path into it's parts"""
|
||||||
|
for stest in VALID_TESTS:
|
||||||
|
result = self._plugin._field_split(stest["path"])
|
||||||
|
self.assertEqual(result, stest["split"])
|
||||||
|
|
||||||
|
def test_missing_var(self):
|
||||||
|
"""Check for a missing fact"""
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b.c", "value": 5}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars={"vars": {}})
|
||||||
|
self.assertIn(
|
||||||
|
"'a' was not found in the current facts.", str(error.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_simple(self):
|
||||||
|
"""Confirm a valid argspec passes"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"] = 5
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b", "value": 5}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_multiple(self):
|
||||||
|
"""Confirm multiple paths passes"""
|
||||||
|
task_vars = {
|
||||||
|
"vars": {"a": {"b1": [1, 2, 3], "b2": {"c": "123", "d": False}}}
|
||||||
|
}
|
||||||
|
expected = {"a": {"b1": [1, 2, 3, 4], "b2": {"c": 456, "d": True}}}
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [
|
||||||
|
{"path": "a.b1.3", "value": 4},
|
||||||
|
{"path": "a.b2.c", "value": 456},
|
||||||
|
{"path": "a.b2.d", "value": True},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_replace_in_list(self):
|
||||||
|
"""Replace in list"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"][1] = 5
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b.1", "value": 5}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_append_to_list(self):
|
||||||
|
"""Append to list"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"].append(4)
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b.3", "value": 4}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_bracket_single_quote(self):
|
||||||
|
"""Bracket notation sigle quote"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"].append(4)
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": "a['b'][3]", "value": 4}]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_bracket_double_quote(self):
|
||||||
|
"""Bracket notation double quote"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"].append(4)
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": 'a["b"][3]', "value": 4}]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_int_dict_keys(self):
|
||||||
|
"""Integer dict keys"""
|
||||||
|
task_vars = {"vars": {"a": {0: [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"][0][0] = 0
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.0.0", "value": 0}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_int_as_string(self):
|
||||||
|
"""Integer dict keys as string"""
|
||||||
|
task_vars = {"vars": {"a": {"0": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["0"][0] = 0
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": 'a["0"].0', "value": 0}]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_invalid_path_quote_after_dot(self):
|
||||||
|
"""Invalid path format"""
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.'b'", "value": 0}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars={"vars": {}})
|
||||||
|
self.assertIn("malformed", str(error.exception))
|
||||||
|
|
||||||
|
def test_run_invalid_path_bracket_after_dot(self):
|
||||||
|
"""Invalid path format"""
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": "a.['b']", "value": 0}]
|
||||||
|
}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars={"vars": {}})
|
||||||
|
self.assertIn("malformed", str(error.exception))
|
||||||
|
|
||||||
|
def test_run_invalid_key_start_with_dot(self):
|
||||||
|
"""Invalid key format"""
|
||||||
|
self._plugin._task.args = {"updates": [{"path": ".abc", "value": 0}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars={"vars": {}})
|
||||||
|
self.assertIn("malformed", str(error.exception))
|
||||||
|
|
||||||
|
def test_run_no_update_list(self):
|
||||||
|
"""Confirm no change when same"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"] = [1, 2, 3]
|
||||||
|
expected.update({"changed": False})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b.0", "value": 1}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_no_update_dict(self):
|
||||||
|
"""Confirm no change when same"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["b"] = [1, 2, 3]
|
||||||
|
expected.update({"changed": False})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": "a.b", "value": [1, 2, 3]}]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_missing_key(self):
|
||||||
|
"""Confirm error when key not found"""
|
||||||
|
task_vars = {"vars": {"a": {"b": 1}}}
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.c.d", "value": 1}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertIn("the key 'c' was not found", str(error.exception))
|
||||||
|
|
||||||
|
def test_run_list_not_int(self):
|
||||||
|
"""Confirm error when key not found"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [1]}}}
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": "a.b['0']", "value": 2}]
|
||||||
|
}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertIn(
|
||||||
|
"index provided was not an integer", str(error.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_list_not_long(self):
|
||||||
|
"""List not long enough"""
|
||||||
|
task_vars = {"vars": {"a": {"b": [0]}}}
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a.b.2", "value": 2}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertIn(
|
||||||
|
"not long enough for item #2 to be set", str(error.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_not_mutable_sequence_or_mapping(self):
|
||||||
|
"""Confirm graceful fail when immutable object
|
||||||
|
This should never happen in the real world
|
||||||
|
"""
|
||||||
|
obj = {"a": frozenset([1, 2, 3])}
|
||||||
|
path = ["a", 0]
|
||||||
|
val = 9
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.set_value(obj, path, val)
|
||||||
|
self.assertIn("can only modify mutable objects", str(error.exception))
|
||||||
|
|
||||||
|
def test_run_not_dotted_success_one(self):
|
||||||
|
"""Test with a not dotted key"""
|
||||||
|
task_vars = {"vars": {"a": 0}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"] = 1
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a", "value": 1}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_not_dotted_success_three(self):
|
||||||
|
"""Test with a not dotted key longer"""
|
||||||
|
task_vars = {"vars": {"abc": 0}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["abc"] = 1
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "abc", "value": 1}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_not_dotted_fail_missing(self):
|
||||||
|
"""Test with a not dotted key, missing"""
|
||||||
|
task_vars = {"vars": {"abc": 0}}
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "123", "value": 1}]}
|
||||||
|
with self.assertRaises(Exception) as error:
|
||||||
|
self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertIn(
|
||||||
|
"'123' was not found in the current facts", str(error.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_not_dotted_success_same(self):
|
||||||
|
"""Test with a not dotted key, no change"""
|
||||||
|
task_vars = {"vars": {"a": 0}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected.update({"changed": False})
|
||||||
|
self._plugin._task.args = {"updates": [{"path": "a", "value": 0}]}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_run_looks_like_a_bool(self):
|
||||||
|
"""Test with a key that looks like a bool"""
|
||||||
|
task_vars = {"vars": {"a": {"True": 0}}}
|
||||||
|
expected = copy.deepcopy(task_vars["vars"])
|
||||||
|
expected["a"]["True"] = 1
|
||||||
|
expected.update({"changed": True})
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"updates": [{"path": "a['True']", "value": 1}]
|
||||||
|
}
|
||||||
|
result = self._plugin.run(task_vars=task_vars)
|
||||||
|
self.assertEqual(result, expected)
|
Loading…
Reference in New Issue