diff --git a/changelogs/fragments/142-dns-regexp.yml b/changelogs/fragments/142-dns-regexp.yml new file mode 100644 index 0000000..b7538ac --- /dev/null +++ b/changelogs/fragments/142-dns-regexp.yml @@ -0,0 +1,6 @@ +minor_changes: + - api_modify, api_info - add field ``regexp`` to ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + +bugfixes: + - api_modify, api_info - do not crash if router contains ``regexp`` DNS entries in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + - api_modify - do not use ``name`` as a unique key in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). diff --git a/plugins/module_utils/_api_data.py b/plugins/module_utils/_api_data.py index 9d4b4c8..038ddef 100644 --- a/plugins/module_utils/_api_data.py +++ b/plugins/module_utils/_api_data.py @@ -13,6 +13,8 @@ __metaclass__ = type class APIData(object): def __init__(self, primary_keys=None, stratify_keys=None, + required_one_of=None, + mutually_exclusive=None, has_identifier=False, single_value=False, unknown_mechanism=False, @@ -25,6 +27,8 @@ class APIData(object): raise ValueError('unknown_mechanism and fully_understood cannot be combined') self.primary_keys = primary_keys self.stratify_keys = stratify_keys + self.required_one_of = required_one_of or [] + self.mutually_exclusive = mutually_exclusive or [] self.has_identifier = has_identifier self.single_value = single_value self.unknown_mechanism = unknown_mechanism @@ -43,6 +47,20 @@ class APIData(object): for sk in stratify_keys: if sk not in fields: raise ValueError('Stratify key {sk} must be in fields!'.format(sk=sk)) + if required_one_of: + for index, require_list in enumerate(required_one_of): + if not isinstance(require_list, list): + raise ValueError('Require one of element at index #{index} must be a list!'.format(index=index + 1)) + for rk in require_list: + if rk not in fields: + raise ValueError('Require one of key {rk} must be in fields!'.format(rk=rk)) + if mutually_exclusive: + for index, exclusive_list in enumerate(mutually_exclusive): + if not isinstance(exclusive_list, list): + raise ValueError('Mutually exclusive element at index #{index} must be a list!'.format(index=index + 1)) + for ek in exclusive_list: + if ek not in fields: + raise ValueError('Mutually exclusive key {ek} must be in fields!'.format(ek=ek)) class KeyInfo(object): @@ -1356,7 +1374,8 @@ PATHS = { ), ('ip', 'dns', 'static'): APIData( fully_understood=True, - stratify_keys=('name', ), + required_one_of=[['name', 'regexp']], + mutually_exclusive=[['name', 'regexp']], fields={ 'address': KeyInfo(), 'cname': KeyInfo(), @@ -1365,8 +1384,9 @@ PATHS = { 'forward-to': KeyInfo(), 'mx-exchange': KeyInfo(), 'mx-preference': KeyInfo(), - 'name': KeyInfo(required=True), + 'name': KeyInfo(), 'ns': KeyInfo(), + 'regexp': KeyInfo(), 'srv-port': KeyInfo(), 'srv-priority': KeyInfo(), 'srv-target': KeyInfo(), diff --git a/plugins/modules/api_modify.py b/plugins/modules/api_modify.py index d03a860..9501331 100644 --- a/plugins/modules/api_modify.py +++ b/plugins/modules/api_modify.py @@ -465,6 +465,24 @@ def polish_entry(entry, path_info, module, for_text): for key, field_info in path_info.fields.items(): if field_info.required and key not in entry: module.fail_json(msg='Key "{key}" must be present{for_text}.'.format(key=key, for_text=for_text)) + for require_list in path_info.required_one_of: + found_req_keys = [rk for rk in require_list if rk in entry] + if len(require_list) > 0 and not found_req_keys: + module.fail_json( + msg='Every element in data must contain one of {required_keys}. For example, the element{for_text} does not provide it.'.format( + required_keys=', '.join(['"{k}"'.format(k=k) for k in require_list]), + for_text=for_text, + ) + ) + for exclusive_list in path_info.mutually_exclusive: + found_ex_keys = [ek for ek in exclusive_list if ek in entry] + if len(found_ex_keys) > 1: + module.fail_json( + msg='Keys {exclusive_keys} cannot be used at the same time{for_text}.'.format( + exclusive_keys=', '.join(['"{k}"'.format(k=k) for k in found_ex_keys]), + for_text=for_text, + ) + ) def remove_irrelevant_data(entry, path_info): diff --git a/tests/unit/plugins/module_utils/test__api_data.py b/tests/unit/plugins/module_utils/test__api_data.py index 6220e29..941a0c2 100644 --- a/tests/unit/plugins/module_utils/test__api_data.py +++ b/tests/unit/plugins/module_utils/test__api_data.py @@ -54,6 +54,22 @@ def test_api_data_errors(): APIData(stratify_keys=['foo'], fields={}) assert exc.value.args[0] == 'Stratify key foo must be in fields!' + with pytest.raises(ValueError) as exc: + APIData(required_one_of=['foo'], fields={}) + assert exc.value.args[0] == 'Require one of element at index #1 must be a list!' + + with pytest.raises(ValueError) as exc: + APIData(required_one_of=[['foo']], fields={}) + assert exc.value.args[0] == 'Require one of key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + APIData(mutually_exclusive=['foo'], fields={}) + assert exc.value.args[0] == 'Mutually exclusive element at index #1 must be a list!' + + with pytest.raises(ValueError) as exc: + APIData(mutually_exclusive=[['foo']], fields={}) + assert exc.value.args[0] == 'Mutually exclusive key foo must be in fields!' + def test_key_info_errors(): values = [ diff --git a/tests/unit/plugins/modules/test_api_modify.py b/tests/unit/plugins/modules/test_api_modify.py index 2c6e0bd..f303ccf 100644 --- a/tests/unit/plugins/modules/test_api_modify.py +++ b/tests/unit/plugins/modules/test_api_modify.py @@ -385,9 +385,9 @@ class TestRouterosApiModifyModule(ModuleTestCase): with self.assertRaises(AnsibleFailJson) as exc: args = self.config_module_args.copy() args.update({ - 'path': 'ip dns static', + 'path': 'ip dhcp-server', 'data': [{ - 'address': '1.2.3.4', + 'interface': 'eth0', }], }) set_module_args(args) @@ -397,6 +397,40 @@ class TestRouterosApiModifyModule(ModuleTestCase): self.assertEqual(result['failed'], True) self.assertEqual(result['msg'], 'Every element in data must contain "name". For example, the element at index #1 does not provide it.') + def test_invalid_required_one_of_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'address': '192.168.88.1', + }], + }) + set_module_args(args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Every element in data must contain one of "name", "regexp". For example, the element at index 1 does not provide it.') + + def test_invalid_mutually_exclusive_both(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'foo', + 'regexp': 'bar', + 'address': '192.168.88.1', + }], + }) + set_module_args(args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Keys "name", "regexp" cannot be used at the same time at index 1.') + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) def test_sync_list_idempotent(self):