Skip to content

Commit

Permalink
Refactor and allow to match by regex.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Aug 11, 2024
1 parent 3f8c90c commit 6951f8c
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 30 deletions.
9 changes: 8 additions & 1 deletion plugins/doc_fragments/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,14 @@ class ModuleDocFragment(object):
then this will not match. If you are not sure, better include both variants:
both the string and the integer.
- Use V(none) for disabled values.
required: true
- Either O(restrict[].values) or O(restrict[].regex), but not both, must be specified.
type: list
elements: raw
regex:
description:
- A regular expression matching values of the field to limit to.
- Note that all values will be converted to strings before matching.
- It is not possible to match disabled values with regular expressions.
- Either O(restrict[].values) or O(restrict[].regex), but not both, must be specified.
type: str
'''
56 changes: 46 additions & 10 deletions plugins/module_utils/_api_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,63 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type

import re

def validate_restrict(module, path_info):
from ansible.module_utils.common.text.converters import to_text


def validate_and_prepare_restrict(module, path_info):
restrict = module.params['restrict']
if restrict is None:
return
for entry in restrict:
field = entry['field']
return None
restrict_data = []
for rule in restrict:
field = rule['field']
if field.startswith('!'):
module.fail_json(msg='restrict: the field name "{0}" must not start with "!"'.format(field))
f = path_info.fields.get(field)
if f is None:
module.fail_json(msg='restrict: the field "{0}" does not exist for this path'.format(field))

new_rule = dict(field=field)
if rule['values'] is not None:
new_rule['values'] = rule['values']
elif rule['regex'] is not None:
regex = rule['regex']
try:
new_rule['regex'] = re.compile(regex)
new_rule['regex_source'] = regex
except Exception as exc:
module.fail_json(msg='restrict: invalid regular expression "{0}": {1}'.format(regex, exc))
restrict_data.append(new_rule)
return restrict_data

def entry_accepted(entry, path_info, module):
restrict = module.params['restrict']
if restrict is None:

def restrict_entry_accepted(entry, path_info, restrict_data):
if restrict_data is None:
return True
for rule in restrict:
for rule in restrict_data:
# Obtain field and value
field = rule['field']
field_info = path_info.fields[field]
value = entry.get(field)
if value is None:
value = field_info.default
if field not in entry and field_info.absent_value:
value = field_info.absent_value
if value not in rule['values']:

# Actual test
if 'values' in rule and value not in rule['values']:
return False
if 'regex' in rule:
if value is None:
# regex cannot match None
return False
value_str = to_text(value)
if isinstance(value, bool):
value_str = value_str.lower()
if rule['regex'].match(value_str):
return False
return True


Expand All @@ -47,7 +76,14 @@ def restrict_argument_spec():
elements='dict',
options=dict(
field=dict(type='str', required=True),
values=dict(type='list', elements='raw', required=True),
values=dict(type='list', elements='raw'),
regex=dict(type='str'),
),
mutually_exclusive=[
('values', 'regex'),
],
required_one_of=[
('values', 'regex'),
],
),
)
8 changes: 4 additions & 4 deletions plugins/modules/api_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@
)

from ansible_collections.community.routeros.plugins.module_utils._api_helper import (
entry_accepted,
restrict_argument_spec,
validate_restrict,
restrict_entry_accepted,
validate_and_prepare_restrict,
)

try:
Expand Down Expand Up @@ -435,7 +435,7 @@ def main():
include_dynamic = module.params['include_dynamic']
include_builtin = module.params['include_builtin']
include_read_only = module.params['include_read_only']
validate_restrict(module, path_info)
restrict_data = validate_and_prepare_restrict(module, path_info)
try:
api_path = compose_api_path(api, path)

Expand All @@ -448,7 +448,7 @@ def main():
if not include_builtin:
if entry.get('builtin', False):
continue
if not entry_accepted(entry, path_info, module):
if not restrict_entry_accepted(entry, path_info, restrict_data):
continue
if not unfiltered:
for k in list(entry):
Expand Down
30 changes: 15 additions & 15 deletions plugins/modules/api_modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,9 @@
)

from ansible_collections.community.routeros.plugins.module_utils._api_helper import (
entry_accepted,
restrict_argument_spec,
validate_restrict,
restrict_entry_accepted,
validate_and_prepare_restrict,
)

HAS_ORDEREDDICT = True
Expand Down Expand Up @@ -732,14 +732,14 @@ def prepare_for_add(entry, path_info):
return new_entry


def remove_rejected(data, path_info, module):
def remove_rejected(data, path_info, restrict_data):
return [
entry for entry in data
if entry_accepted(entry, path_info, module)
if restrict_entry_accepted(entry, path_info, restrict_data)
]


def sync_list(module, api, path, path_info):
def sync_list(module, api, path, path_info, restrict_data):
handle_absent_entries = module.params['handle_absent_entries']
handle_entries_content = module.params['handle_entries_content']
if handle_absent_entries == 'remove':
Expand All @@ -753,7 +753,7 @@ def sync_list(module, api, path, path_info):
data = module.params['data']
stratified_data = defaultdict(list)
for index, entry in enumerate(data):
if not entry_accepted(entry, path_info, module):
if not restrict_entry_accepted(entry, path_info, restrict_data):
module.fail_json(msg='The element at index #{index} does not match `restrict`'.format(index=index + 1))
for stratify_key in stratify_keys:
if stratify_key not in entry:
Expand All @@ -775,7 +775,7 @@ def sync_list(module, api, path, path_info):

old_data = get_api_data(api_path, path_info)
old_data = remove_dynamic(old_data)
old_data = remove_rejected(old_data, path_info, module)
old_data = remove_rejected(old_data, path_info, restrict_data)
stratified_old_data = defaultdict(list)
for index, entry in enumerate(old_data):
sks = tuple(entry[stratify_key] for stratify_key in stratify_keys)
Expand Down Expand Up @@ -888,7 +888,7 @@ def match(current_entry):
# For sake of completeness, retrieve the full new data:
if modify_list or create_list or reorder_list:
new_data = remove_dynamic(get_api_data(api_path, path_info))
new_data = remove_rejected(new_data, path_info, module)
new_data = remove_rejected(new_data, path_info, restrict_data)

# Remove 'irrelevant' data
for entry in old_data:
Expand All @@ -915,7 +915,7 @@ def match(current_entry):
)


def sync_with_primary_keys(module, api, path, path_info):
def sync_with_primary_keys(module, api, path, path_info, restrict_data):
primary_keys = path_info.primary_keys

if path_info.fixed_entries:
Expand All @@ -927,7 +927,7 @@ def sync_with_primary_keys(module, api, path, path_info):
data = module.params['data']
new_data_by_key = OrderedDict()
for index, entry in enumerate(data):
if not entry_accepted(entry, path_info, module):
if not restrict_entry_accepted(entry, path_info, restrict_data):
module.fail_json(msg='The element at index #{index} does not match `restrict`'.format(index=index + 1))
for primary_key in primary_keys:
if primary_key not in entry:
Expand Down Expand Up @@ -960,7 +960,7 @@ def sync_with_primary_keys(module, api, path, path_info):

old_data = get_api_data(api_path, path_info)
old_data = remove_dynamic(old_data)
old_data = remove_rejected(old_data, path_info, module)
old_data = remove_rejected(old_data, path_info, restrict_data)
old_data_by_key = OrderedDict()
id_by_key = {}
for entry in old_data:
Expand Down Expand Up @@ -1087,7 +1087,7 @@ def sync_with_primary_keys(module, api, path, path_info):
# For sake of completeness, retrieve the full new data:
if modify_list or create_list or reorder_list:
new_data = remove_dynamic(get_api_data(api_path, path_info))
new_data = remove_rejected(new_data, path_info, module)
new_data = remove_rejected(new_data, path_info, restrict_data)

# Remove 'irrelevant' data
for entry in old_data:
Expand All @@ -1114,7 +1114,7 @@ def sync_with_primary_keys(module, api, path, path_info):
)


def sync_single_value(module, api, path, path_info):
def sync_single_value(module, api, path, path_info, restrict_data):
if module.params['restrict'] is not None:
module.fail_json(msg='The restrict option cannot be used with this path, since there is precisely one entry.')
data = module.params['data']
Expand Down Expand Up @@ -1246,9 +1246,9 @@ def main():
if path_info is None or backend is None:
module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path)))

validate_restrict(module, path_info)
restrict_data = validate_and_prepare_restrict(module, path_info)

backend(module, api, path, path_info)
backend(module, api, path, path_info, restrict_data)


if __name__ == '__main__':
Expand Down

0 comments on commit 6951f8c

Please sign in to comment.