From 6951f8cd01abbd2f341bd843577c2f429e37c8ce Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 11 Aug 2024 21:52:59 +0200 Subject: [PATCH] Refactor and allow to match by regex. --- plugins/doc_fragments/api.py | 9 ++++- plugins/module_utils/_api_helper.py | 56 +++++++++++++++++++++++------ plugins/modules/api_info.py | 8 ++--- plugins/modules/api_modify.py | 30 ++++++++-------- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/plugins/doc_fragments/api.py b/plugins/doc_fragments/api.py index ef6228ca..c10d844f 100644 --- a/plugins/doc_fragments/api.py +++ b/plugins/doc_fragments/api.py @@ -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 ''' diff --git a/plugins/module_utils/_api_helper.py b/plugins/module_utils/_api_helper.py index 52392766..505397fb 100644 --- a/plugins/module_utils/_api_helper.py +++ b/plugins/module_utils/_api_helper.py @@ -9,25 +9,43 @@ 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) @@ -35,8 +53,19 @@ def entry_accepted(entry, path_info, module): 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 @@ -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'), + ], ), ) diff --git a/plugins/modules/api_info.py b/plugins/modules/api_info.py index f1a56535..24fd8fed 100644 --- a/plugins/modules/api_info.py +++ b/plugins/modules/api_info.py @@ -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: @@ -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) @@ -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): diff --git a/plugins/modules/api_modify.py b/plugins/modules/api_modify.py index ca9ea590..a2a2c102 100644 --- a/plugins/modules/api_modify.py +++ b/plugins/modules/api_modify.py @@ -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 @@ -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': @@ -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: @@ -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) @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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'] @@ -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__':