mirror of
https://github.com/ansible-collections/community.routeros.git
synced 2025-06-23 18:28:42 +02:00
Add api_find_and_modify module. (#93)
This commit is contained in:
parent
d57de117f5
commit
ff66ba9289
7 changed files with 2978 additions and 0 deletions
|
@ -4,3 +4,4 @@ action_groups:
|
||||||
api:
|
api:
|
||||||
- api
|
- api
|
||||||
- api_facts
|
- api_facts
|
||||||
|
- api_find_and_modify
|
||||||
|
|
1798
plugins/module_utils/_api_data.py
Normal file
1798
plugins/module_utils/_api_data.py
Normal file
File diff suppressed because it is too large
Load diff
312
plugins/modules/api_find_and_modify.py
Normal file
312
plugins/modules/api_find_and_modify.py
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: api_find_and_modify
|
||||||
|
author:
|
||||||
|
- "Felix Fontein (@felixfontein)"
|
||||||
|
short_description: Find and modify information using the API
|
||||||
|
version_added: 2.1.0
|
||||||
|
description:
|
||||||
|
- Allows to find entries for a path by conditions and modify the values of these entries.
|
||||||
|
notes:
|
||||||
|
- "If you want to change values based on their old values (like change all comments 'foo' to 'bar') and make sure that
|
||||||
|
there are at least N such values, you can use I(require_matches_min=N) together with I(allow_no_matches=true).
|
||||||
|
This will make the module fail if there are less than N such entries, but not if there is no match. The latter case
|
||||||
|
is needed for idempotency of the task: once the values have been changed, there should be no further match."
|
||||||
|
- Supports I(check_mode).
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.routeros.api
|
||||||
|
options:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- Path to query.
|
||||||
|
- An example value is C(ip address). This is equivalent to running C(/ip address) in the RouterOS CLI.
|
||||||
|
required: true
|
||||||
|
type: str
|
||||||
|
find:
|
||||||
|
description:
|
||||||
|
- Fields to search for.
|
||||||
|
- The module will only consider entries in the given I(path) that match all fields provided here.
|
||||||
|
- Use YAML C(~), or prepend keys with C(!), to specify an unset value.
|
||||||
|
- Note that if the dictionary specified here is empty, every entry in the path will be matched.
|
||||||
|
required: true
|
||||||
|
type: dict
|
||||||
|
values:
|
||||||
|
description:
|
||||||
|
- On all entries matching the conditions in I(find), set the keys of this option to the values specified here.
|
||||||
|
- Use YAML C(~), or prepend keys with C(!), to specify to unset a value.
|
||||||
|
required: true
|
||||||
|
type: dict
|
||||||
|
require_matches_min:
|
||||||
|
description:
|
||||||
|
- Make sure that there are no less matches than this number.
|
||||||
|
- If there are less matches, fail instead of modifying anything.
|
||||||
|
type: int
|
||||||
|
default: 0
|
||||||
|
require_matches_max:
|
||||||
|
description:
|
||||||
|
- Make sure that there are no more matches than this number.
|
||||||
|
- If there are more matches, fail instead of modifying anything.
|
||||||
|
- If not specified, there is no upper limit.
|
||||||
|
type: int
|
||||||
|
allow_no_matches:
|
||||||
|
description:
|
||||||
|
- Whether to allow that no match is found.
|
||||||
|
- If not specified, this value is induced from whether I(require_matches_min) is 0 or larger.
|
||||||
|
type: bool
|
||||||
|
seealso:
|
||||||
|
- module: community.routeros.api
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
---
|
||||||
|
- name: Rename bridge from 'bridge' to 'my-bridge'
|
||||||
|
community.routeros.api_find_and_modify:
|
||||||
|
hostname: "{{ hostname }}"
|
||||||
|
password: "{{ password }}"
|
||||||
|
username: "{{ username }}"
|
||||||
|
path: interface bridge
|
||||||
|
find:
|
||||||
|
name: bridge
|
||||||
|
values:
|
||||||
|
name: my-bridge
|
||||||
|
|
||||||
|
- name: Change IP address to 192.168.1.1 for interface bridge - assuming there is only one
|
||||||
|
community.routeros.api_find_and_modify:
|
||||||
|
hostname: "{{ hostname }}"
|
||||||
|
password: "{{ password }}"
|
||||||
|
username: "{{ username }}"
|
||||||
|
path: ip address
|
||||||
|
find:
|
||||||
|
interface: bridge
|
||||||
|
values:
|
||||||
|
address: "192.168.1.1/24"
|
||||||
|
# If there are zero entries, or more than one: fail! We expected that
|
||||||
|
# exactly one is configured.
|
||||||
|
require_matches_min: 1
|
||||||
|
require_matches_max: 1
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
old_data:
|
||||||
|
description:
|
||||||
|
- A list of all elements for the current path before a change was made.
|
||||||
|
sample:
|
||||||
|
- '.id': '*1'
|
||||||
|
actual-interface: bridge
|
||||||
|
address: "192.168.88.1/24"
|
||||||
|
comment: defconf
|
||||||
|
disabled: false
|
||||||
|
dynamic: false
|
||||||
|
interface: bridge
|
||||||
|
invalid: false
|
||||||
|
network: 192.168.88.0
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
returned: success
|
||||||
|
new_data:
|
||||||
|
description:
|
||||||
|
- A list of all elements for the current path after a change was made.
|
||||||
|
sample:
|
||||||
|
- '.id': '*1'
|
||||||
|
actual-interface: bridge
|
||||||
|
address: "192.168.1.1/24"
|
||||||
|
comment: awesome
|
||||||
|
disabled: false
|
||||||
|
dynamic: false
|
||||||
|
interface: bridge
|
||||||
|
invalid: false
|
||||||
|
network: 192.168.1.0
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
returned: success
|
||||||
|
match_count:
|
||||||
|
description:
|
||||||
|
- The number of entries that matched the criteria in I(find).
|
||||||
|
sample: 1
|
||||||
|
type: int
|
||||||
|
returned: success
|
||||||
|
modify__count:
|
||||||
|
description:
|
||||||
|
- The number of entries that were modified.
|
||||||
|
sample: 1
|
||||||
|
type: int
|
||||||
|
returned: success
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.routeros.plugins.module_utils.api import (
|
||||||
|
api_argument_spec,
|
||||||
|
check_has_library,
|
||||||
|
create_api,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.routeros.plugins.module_utils._api_data import (
|
||||||
|
split_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from librouteros.exceptions import LibRouterosError
|
||||||
|
except Exception:
|
||||||
|
# Handled in api module_utils
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def compose_api_path(api, path):
|
||||||
|
api_path = api.path()
|
||||||
|
for p in path:
|
||||||
|
api_path = api_path.join(p)
|
||||||
|
return api_path
|
||||||
|
|
||||||
|
|
||||||
|
DISABLED_MEANS_EMPTY_STRING = ('comment', )
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module_args = dict(
|
||||||
|
path=dict(type='str', required=True),
|
||||||
|
find=dict(type='dict', required=True),
|
||||||
|
values=dict(type='dict', required=True),
|
||||||
|
require_matches_min=dict(type='int', default=0),
|
||||||
|
require_matches_max=dict(type='int'),
|
||||||
|
allow_no_matches=dict(type='bool'),
|
||||||
|
)
|
||||||
|
module_args.update(api_argument_spec())
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=module_args,
|
||||||
|
supports_check_mode=True,
|
||||||
|
)
|
||||||
|
if module.params['allow_no_matches'] is None:
|
||||||
|
module.params['allow_no_matches'] = module.params['require_matches_min'] <= 0
|
||||||
|
|
||||||
|
find = module.params['find']
|
||||||
|
for key, value in sorted(find.items()):
|
||||||
|
if key.startswith('!'):
|
||||||
|
key = key[1:]
|
||||||
|
if value not in (None, ''):
|
||||||
|
module.fail_json(msg='The value for "!{key}" in `find` must not be non-trivial!'.format(key=key))
|
||||||
|
if key in find:
|
||||||
|
module.fail_json(msg='`find` must not contain both "{key}" and "!{key}"!'.format(key=key))
|
||||||
|
values = module.params['values']
|
||||||
|
for key, value in sorted(values.items()):
|
||||||
|
if key.startswith('!'):
|
||||||
|
key = key[1:]
|
||||||
|
if value not in (None, ''):
|
||||||
|
module.fail_json(msg='The value for "!{key}" in `values` must not be non-trivial!'.format(key=key))
|
||||||
|
if key in values:
|
||||||
|
module.fail_json(msg='`values` must not contain both "{key}" and "!{key}"!'.format(key=key))
|
||||||
|
|
||||||
|
check_has_library(module)
|
||||||
|
api = create_api(module)
|
||||||
|
|
||||||
|
path = split_path(module.params['path'])
|
||||||
|
|
||||||
|
api_path = compose_api_path(api, path)
|
||||||
|
|
||||||
|
old_data = list(api_path)
|
||||||
|
new_data = [entry.copy() for entry in old_data]
|
||||||
|
|
||||||
|
# Find matching entries
|
||||||
|
matching_entries = []
|
||||||
|
for index, entry in enumerate(new_data):
|
||||||
|
matches = True
|
||||||
|
for key, value in find.items():
|
||||||
|
if key.startswith('!'):
|
||||||
|
# Allow to specify keys that should not be present by prepending '!'
|
||||||
|
key = key[1:]
|
||||||
|
value = None
|
||||||
|
current_value = entry.get(key)
|
||||||
|
if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None:
|
||||||
|
current_value = value
|
||||||
|
if current_value != value:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
if matches:
|
||||||
|
matching_entries.append((index, entry))
|
||||||
|
|
||||||
|
# Check whether the correct amount of entries was found
|
||||||
|
if matching_entries:
|
||||||
|
if len(matching_entries) < module.params['require_matches_min']:
|
||||||
|
module.fail_json(msg='Found %d entries, but expected at least %d' % (len(matching_entries), module.params['require_matches_min']))
|
||||||
|
if module.params['require_matches_max'] is not None and len(matching_entries) > module.params['require_matches_max']:
|
||||||
|
module.fail_json(msg='Found %d entries, but expected at most %d' % (len(matching_entries), module.params['require_matches_max']))
|
||||||
|
elif not module.params['allow_no_matches']:
|
||||||
|
module.fail_json(msg='Found no entries, but allow_no_matches=false')
|
||||||
|
|
||||||
|
# Identify entries to update
|
||||||
|
modifications = []
|
||||||
|
for index, entry in matching_entries:
|
||||||
|
modification = {}
|
||||||
|
for key, value in values.items():
|
||||||
|
if key.startswith('!'):
|
||||||
|
# Allow to specify keys to remove by prepending '!'
|
||||||
|
key = key[1:]
|
||||||
|
value = None
|
||||||
|
current_value = entry.get(key)
|
||||||
|
if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None:
|
||||||
|
current_value = value
|
||||||
|
if current_value != value:
|
||||||
|
if value is None:
|
||||||
|
disable_key = '!%s' % key
|
||||||
|
if key in DISABLED_MEANS_EMPTY_STRING:
|
||||||
|
disable_key = key
|
||||||
|
modification[disable_key] = ''
|
||||||
|
entry.pop(key, None)
|
||||||
|
else:
|
||||||
|
modification[key] = value
|
||||||
|
entry[key] = value
|
||||||
|
if modification:
|
||||||
|
if '.id' in entry:
|
||||||
|
modification['.id'] = entry['.id']
|
||||||
|
modifications.append(modification)
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
if not module.check_mode and modifications:
|
||||||
|
for modification in modifications:
|
||||||
|
try:
|
||||||
|
api_path.update(**modification)
|
||||||
|
except LibRouterosError as e:
|
||||||
|
module.fail_json(
|
||||||
|
msg='Error while modifying for .id={id}: {error}'.format(
|
||||||
|
id=modification['.id'],
|
||||||
|
error=to_native(e),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
new_data = list(api_path)
|
||||||
|
|
||||||
|
# Produce return value
|
||||||
|
more = {}
|
||||||
|
if module._diff:
|
||||||
|
# Only include the matching values
|
||||||
|
more['diff'] = {
|
||||||
|
'before': {
|
||||||
|
'values': [old_data[index] for index, entry in matching_entries],
|
||||||
|
},
|
||||||
|
'after': {
|
||||||
|
'values': [entry for index, entry in matching_entries],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
module.exit_json(
|
||||||
|
changed=bool(modifications),
|
||||||
|
old_data=old_data,
|
||||||
|
new_data=new_data,
|
||||||
|
match_count=len(matching_entries),
|
||||||
|
modify_count=len(modifications),
|
||||||
|
**more
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
91
tests/unit/plugins/module_utils/test__api_data.py
Normal file
91
tests/unit/plugins/module_utils/test__api_data.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de>
|
||||||
|
# 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 pytest
|
||||||
|
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.routeros.plugins.module_utils._api_data import (
|
||||||
|
APIData,
|
||||||
|
KeyInfo,
|
||||||
|
split_path,
|
||||||
|
join_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_data_errors():
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData()
|
||||||
|
assert exc.value.args[0] == 'fields must be provided'
|
||||||
|
|
||||||
|
values = [
|
||||||
|
('primary_keys', []),
|
||||||
|
('stratify_keys', []),
|
||||||
|
('has_identifier', True),
|
||||||
|
('single_value', True),
|
||||||
|
('unknown_mechanism', True),
|
||||||
|
]
|
||||||
|
|
||||||
|
for index, (param, param_value) in enumerate(values):
|
||||||
|
for param2, param2_value in values[index + 1:]:
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData(**{param: param_value, param2: param2_value})
|
||||||
|
assert exc.value.args[0] == 'primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData(unknown_mechanism=True, fully_understood=True)
|
||||||
|
assert exc.value.args[0] == 'unknown_mechanism and fully_understood cannot be combined'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData(unknown_mechanism=True, fixed_entries=True)
|
||||||
|
assert exc.value.args[0] == 'fixed_entries can only be used with primary_keys'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData(primary_keys=['foo'], fields={})
|
||||||
|
assert exc.value.args[0] == 'Primary key foo must be in fields!'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
APIData(stratify_keys=['foo'], fields={})
|
||||||
|
assert exc.value.args[0] == 'Stratify key foo must be in fields!'
|
||||||
|
|
||||||
|
|
||||||
|
def test_key_info_errors():
|
||||||
|
values = [
|
||||||
|
('required', True),
|
||||||
|
('default', ''),
|
||||||
|
('automatically_computed_from', ()),
|
||||||
|
('can_disable', True),
|
||||||
|
]
|
||||||
|
|
||||||
|
for index, (param, param_value) in enumerate(values):
|
||||||
|
for param2, param2_value in values[index + 1:]:
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
KeyInfo(**{param: param_value, param2: param2_value})
|
||||||
|
assert exc.value.args[0] == 'required, default, automatically_computed_from, and can_disable are mutually exclusive'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
KeyInfo('foo')
|
||||||
|
assert exc.value.args[0] == 'KeyInfo() does not have positional arguments'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc:
|
||||||
|
KeyInfo(remove_value='')
|
||||||
|
assert exc.value.args[0] == 'remove_value can only be specified if can_disable=True'
|
||||||
|
|
||||||
|
|
||||||
|
SPLITTED_PATHS = [
|
||||||
|
('', [], ''),
|
||||||
|
(' ip ', ['ip'], 'ip'),
|
||||||
|
('ip', ['ip'], 'ip'),
|
||||||
|
(' ip \t\n\raddress ', ['ip', 'address'], 'ip address'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("joined_input, splitted, joined_output", SPLITTED_PATHS)
|
||||||
|
def test_join_split_path(joined_input, splitted, joined_output):
|
||||||
|
assert split_path(joined_input) == splitted
|
||||||
|
assert join_path(splitted) == joined_output
|
|
@ -17,6 +17,8 @@
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS
|
||||||
|
|
||||||
|
|
||||||
class FakeLibRouterosError(Exception):
|
class FakeLibRouterosError(Exception):
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
|
@ -125,3 +127,110 @@ class Or(object):
|
||||||
|
|
||||||
def str_return(self):
|
def str_return(self):
|
||||||
return repr(self.args)
|
return repr(self.args)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_entry(entry, path_info):
|
||||||
|
for key, data in path_info.fields.items():
|
||||||
|
if key not in entry and data.default is not None:
|
||||||
|
entry[key] = data.default
|
||||||
|
if data.can_disable:
|
||||||
|
if key in entry and entry[key] in (None, data.remove_value):
|
||||||
|
del entry[key]
|
||||||
|
if ('!%s' % key) in entry:
|
||||||
|
entry.pop(key, None)
|
||||||
|
del entry['!%s' % key]
|
||||||
|
|
||||||
|
|
||||||
|
def massage_expected_result_data(values, path, keep_all=False):
|
||||||
|
path_info = PATHS[path]
|
||||||
|
values = [entry.copy() for entry in values]
|
||||||
|
for entry in values:
|
||||||
|
_normalize_entry(entry, path_info)
|
||||||
|
if not keep_all:
|
||||||
|
for key in list(entry):
|
||||||
|
if key == '.id' or key in path_info.fields:
|
||||||
|
continue
|
||||||
|
del entry[key]
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class Path(object):
|
||||||
|
def __init__(self, path, initial_values, read_only=False):
|
||||||
|
self._path = path
|
||||||
|
self._path_info = PATHS[path]
|
||||||
|
self._values = [entry.copy() for entry in initial_values]
|
||||||
|
for entry in self._values:
|
||||||
|
_normalize_entry(entry, self._path_info)
|
||||||
|
self._new_id_counter = 0
|
||||||
|
self._read_only = read_only
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return [entry.copy() for entry in self._values].__iter__()
|
||||||
|
|
||||||
|
def _find_id(self, id, required=False):
|
||||||
|
for index, entry in enumerate(self._values):
|
||||||
|
if entry['.id'] == id:
|
||||||
|
return index
|
||||||
|
if required:
|
||||||
|
raise FakeLibRouterosError('Cannot find key "%s"' % id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add(self, **kwargs):
|
||||||
|
if self._path_info.fixed_entries or self._path_info.single_value:
|
||||||
|
raise Exception('Cannot add entries')
|
||||||
|
if self._read_only:
|
||||||
|
raise Exception('Modifying read-only path: add %s' % repr(kwargs))
|
||||||
|
if '.id' in kwargs:
|
||||||
|
raise Exception('Trying to create new entry with ".id" field: %s' % repr(kwargs))
|
||||||
|
self._new_id_counter += 1
|
||||||
|
id = '*NEW%d' % self._new_id_counter
|
||||||
|
entry = {
|
||||||
|
'.id': id,
|
||||||
|
}
|
||||||
|
entry.update(kwargs)
|
||||||
|
_normalize_entry(entry, self._path_info)
|
||||||
|
self._values.append(entry)
|
||||||
|
return id
|
||||||
|
|
||||||
|
def remove(self, *args):
|
||||||
|
if self._path_info.fixed_entries or self._path_info.single_value:
|
||||||
|
raise Exception('Cannot remove entries')
|
||||||
|
if self._read_only:
|
||||||
|
raise Exception('Modifying read-only path: remove %s' % repr(args))
|
||||||
|
for id in args:
|
||||||
|
index = self._find_id(id, required=True)
|
||||||
|
del self._values[index]
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
if self._read_only:
|
||||||
|
raise Exception('Modifying read-only path: update %s' % repr(kwargs))
|
||||||
|
if self._path_info.single_value:
|
||||||
|
index = 0
|
||||||
|
else:
|
||||||
|
index = self._find_id(kwargs['.id'], required=True)
|
||||||
|
entry = self._values[index]
|
||||||
|
entry.update(kwargs)
|
||||||
|
_normalize_entry(entry, self._path_info)
|
||||||
|
|
||||||
|
def __call__(self, command, *args, **kwargs):
|
||||||
|
if self._read_only:
|
||||||
|
raise Exception('Modifying read-only path: "%s" %s %s' % (command, repr(args), repr(kwargs)))
|
||||||
|
if command != 'move':
|
||||||
|
raise FakeLibRouterosError('Unsupported command "%s"' % command)
|
||||||
|
if self._path_info.fixed_entries or self._path_info.single_value:
|
||||||
|
raise Exception('Cannot move entries')
|
||||||
|
yield None # make sure that nothing happens if the result isn't consumed
|
||||||
|
source_index = self._find_id(kwargs.pop('numbers'), required=True)
|
||||||
|
entry = self._values.pop(source_index)
|
||||||
|
dest_index = self._find_id(kwargs.pop('destination'), required=True)
|
||||||
|
self._values.insert(dest_index, entry)
|
||||||
|
|
||||||
|
|
||||||
|
def create_fake_path(path, initial_values, read_only=False):
|
||||||
|
def create(api, called_path):
|
||||||
|
called_path = tuple(called_path)
|
||||||
|
if path != called_path:
|
||||||
|
raise AssertionError('Expected {path}, got {called_path}'.format(path=path, called_path=called_path))
|
||||||
|
return Path(path, initial_values, read_only=read_only)
|
||||||
|
|
||||||
|
return create
|
||||||
|
|
666
tests/unit/plugins/modules/test_api_find_and_modify.py
Normal file
666
tests/unit/plugins/modules/test_api_find_and_modify.py
Normal file
|
@ -0,0 +1,666 @@
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Make coding more python3-ish
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
|
||||||
|
from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import (
|
||||||
|
FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path,
|
||||||
|
)
|
||||||
|
from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
|
||||||
|
from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS
|
||||||
|
from ansible_collections.community.routeros.plugins.modules import api_find_and_modify
|
||||||
|
|
||||||
|
|
||||||
|
START_IP_DNS_STATIC = [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'name': 'router',
|
||||||
|
'address': '192.168.88.1',
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'name': 'router',
|
||||||
|
'text': 'Router Text Entry',
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'comment': '',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
START_IP_DNS_STATIC_OLD_DATA = massage_expected_result_data(START_IP_DNS_STATIC, ('ip', 'dns', 'static'), keep_all=True)
|
||||||
|
|
||||||
|
START_IP_FIREWALL_FILTER = [
|
||||||
|
{
|
||||||
|
'.id': '*2',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'protocol': 'icmp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*3',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'established',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*4',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'related',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'action': 'drop',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'in-interface': 'wan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*8',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'established',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*9',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'related',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'action': 'drop',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-status': 'invalid',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
START_IP_FIREWALL_FILTER_OLD_DATA = massage_expected_result_data(START_IP_FIREWALL_FILTER, ('ip', 'firewall', 'filter'), keep_all=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRouterosApiFindAndModifyModule(ModuleTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestRouterosApiFindAndModifyModule, self).setUp()
|
||||||
|
self.module = api_find_and_modify
|
||||||
|
self.module.LibRouterosError = FakeLibRouterosError
|
||||||
|
self.module.connect = MagicMock(new=fake_ros_api)
|
||||||
|
self.module.check_has_library = MagicMock()
|
||||||
|
self.patch_create_api = patch(
|
||||||
|
'ansible_collections.community.routeros.plugins.modules.api_find_and_modify.create_api',
|
||||||
|
MagicMock(new=fake_ros_api))
|
||||||
|
self.patch_create_api.start()
|
||||||
|
self.config_module_args = {
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'pаss',
|
||||||
|
'hostname': '127.0.0.1',
|
||||||
|
}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.patch_create_api.stop()
|
||||||
|
|
||||||
|
def test_module_fail_when_required_args_missing(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
set_module_args({})
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
|
||||||
|
def test_invalid_disabled_and_enabled_option_in_find(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'comment': 'foo',
|
||||||
|
'!comment': None,
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': 'bar',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], '`find` must not contain both "comment" and "!comment"!')
|
||||||
|
|
||||||
|
def test_invalid_disabled_option_invalid_value_in_find(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'!comment': 'gone',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': 'bar',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], 'The value for "!comment" in `find` must not be non-trivial!')
|
||||||
|
|
||||||
|
def test_invalid_disabled_and_enabled_option_in_values(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {},
|
||||||
|
'values': {
|
||||||
|
'comment': 'foo',
|
||||||
|
'!comment': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], '`values` must not contain both "comment" and "!comment"!')
|
||||||
|
|
||||||
|
def test_invalid_disabled_option_invalid_value_in_values(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {},
|
||||||
|
'values': {
|
||||||
|
'!comment': 'gone',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], 'The value for "!comment" in `values` must not be non-trivial!')
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_change_invalid_zero(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'bam',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'name': 'baz',
|
||||||
|
},
|
||||||
|
'require_matches_min': 10,
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], 'Found no entries, but allow_no_matches=false')
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_change_invalid_too_few(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'router',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'name': 'foobar',
|
||||||
|
},
|
||||||
|
'require_matches_min': 10,
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], 'Found 2 entries, but expected at least 10')
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_change_invalid_too_many(self):
|
||||||
|
with self.assertRaises(AnsibleFailJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'router',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'name': 'foobar',
|
||||||
|
},
|
||||||
|
'require_matches_max': 1,
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['failed'], True)
|
||||||
|
self.assertEqual(result['msg'], 'Found 2 entries, but expected at most 1')
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_change_idempotent_zero_matches_1(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'baz',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'name': 'bam',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], False)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['match_count'], 0)
|
||||||
|
self.assertEqual(result['modify_count'], 0)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_change_idempotent_zero_matches_2(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'baz',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'name': 'bam',
|
||||||
|
},
|
||||||
|
'require_matches_min': 2,
|
||||||
|
'allow_no_matches': True,
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], False)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['match_count'], 0)
|
||||||
|
self.assertEqual(result['modify_count'], 0)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_idempotent_1(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], False)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['match_count'], 3)
|
||||||
|
self.assertEqual(result['modify_count'], 0)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
|
||||||
|
def test_idempotent_2(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'foo',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], False)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['match_count'], 1)
|
||||||
|
self.assertEqual(result['modify_count'], 0)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
|
||||||
|
def test_change(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
'name': 'foo',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': 'bar',
|
||||||
|
},
|
||||||
|
'_ansible_diff': True,
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], True)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'name': 'router',
|
||||||
|
'address': '192.168.88.1',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'name': 'router',
|
||||||
|
'text': 'Router Text Entry',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'comment': 'bar',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['diff']['before']['values'], [
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['diff']['after']['values'], [
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'comment': 'bar',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['match_count'], 1)
|
||||||
|
self.assertEqual(result['modify_count'], 1)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
|
||||||
|
def test_change_remove_comment_1(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], True)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'name': 'router',
|
||||||
|
'address': '192.168.88.1',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'name': 'router',
|
||||||
|
'text': 'Router Text Entry',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual('diff' in result, False)
|
||||||
|
self.assertEqual(result['match_count'], 3)
|
||||||
|
self.assertEqual(result['modify_count'], 1)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
|
||||||
|
def test_change_remove_comment_2(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'comment': '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], True)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'name': 'router',
|
||||||
|
'address': '192.168.88.1',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'name': 'router',
|
||||||
|
'text': 'Router Text Entry',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['match_count'], 3)
|
||||||
|
self.assertEqual(result['modify_count'], 1)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
|
||||||
|
def test_change_remove_comment_3(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dns static',
|
||||||
|
'find': {
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'!comment': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], True)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'name': 'router',
|
||||||
|
'address': '192.168.88.1',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'name': 'router',
|
||||||
|
'text': 'Router Text Entry',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'name': 'foo',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'ttl': '1d',
|
||||||
|
'disabled': False,
|
||||||
|
'dynamic': False,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['match_count'], 3)
|
||||||
|
self.assertEqual(result['modify_count'], 1)
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'firewall', 'filter'), START_IP_FIREWALL_FILTER))
|
||||||
|
def test_change_remove_generic(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip firewall filter',
|
||||||
|
'find': {
|
||||||
|
'chain': 'input',
|
||||||
|
'!protocol': '',
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
'!connection-state': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], True)
|
||||||
|
self.assertEqual(result['old_data'], START_IP_FIREWALL_FILTER_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], [
|
||||||
|
{
|
||||||
|
'.id': '*2',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'protocol': 'icmp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*3',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*4',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*7',
|
||||||
|
'action': 'drop',
|
||||||
|
'chain': 'input',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'in-interface': 'wan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*8',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'established',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*9',
|
||||||
|
'action': 'accept',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-state': 'related',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*A',
|
||||||
|
'action': 'drop',
|
||||||
|
'chain': 'forward',
|
||||||
|
'comment': 'defconf',
|
||||||
|
'connection-status': 'invalid',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.assertEqual(result['match_count'], 3)
|
||||||
|
self.assertEqual(result['modify_count'], 2)
|
|
@ -1 +1,2 @@
|
||||||
unittest2 ; python_version <= '2.6'
|
unittest2 ; python_version <= '2.6'
|
||||||
|
ordereddict ; python_version <= '2.6'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue