Add regexp field to ip dns static (#142)

* Add regexp field to "ip dns static"

* Change test_invalid_required_missing to use "ip dhcp-server"

"ip dns static" requires name or regexp so it cannot be used in this test.

* Add required_one_of attribute to APIData

Used by "ip dns static" which requires either "name" or "regexp.

* Add mutually_exclusive attribute to APIData

Used by "ip dns static" where only one of "name" or "regexp" can be used.

* Add changelog fragment
This commit is contained in:
Andrei Costescu 2022-12-29 08:51:40 +01:00 committed by GitHub
parent 62da7dd4e3
commit 586edbc211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 4 deletions

View file

@ -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).

View file

@ -13,6 +13,8 @@ __metaclass__ = type
class APIData(object): class APIData(object):
def __init__(self, primary_keys=None, def __init__(self, primary_keys=None,
stratify_keys=None, stratify_keys=None,
required_one_of=None,
mutually_exclusive=None,
has_identifier=False, has_identifier=False,
single_value=False, single_value=False,
unknown_mechanism=False, unknown_mechanism=False,
@ -25,6 +27,8 @@ class APIData(object):
raise ValueError('unknown_mechanism and fully_understood cannot be combined') raise ValueError('unknown_mechanism and fully_understood cannot be combined')
self.primary_keys = primary_keys self.primary_keys = primary_keys
self.stratify_keys = stratify_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.has_identifier = has_identifier
self.single_value = single_value self.single_value = single_value
self.unknown_mechanism = unknown_mechanism self.unknown_mechanism = unknown_mechanism
@ -43,6 +47,20 @@ class APIData(object):
for sk in stratify_keys: for sk in stratify_keys:
if sk not in fields: if sk not in fields:
raise ValueError('Stratify key {sk} must be in fields!'.format(sk=sk)) 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): class KeyInfo(object):
@ -1356,7 +1374,8 @@ PATHS = {
), ),
('ip', 'dns', 'static'): APIData( ('ip', 'dns', 'static'): APIData(
fully_understood=True, fully_understood=True,
stratify_keys=('name', ), required_one_of=[['name', 'regexp']],
mutually_exclusive=[['name', 'regexp']],
fields={ fields={
'address': KeyInfo(), 'address': KeyInfo(),
'cname': KeyInfo(), 'cname': KeyInfo(),
@ -1365,8 +1384,9 @@ PATHS = {
'forward-to': KeyInfo(), 'forward-to': KeyInfo(),
'mx-exchange': KeyInfo(), 'mx-exchange': KeyInfo(),
'mx-preference': KeyInfo(), 'mx-preference': KeyInfo(),
'name': KeyInfo(required=True), 'name': KeyInfo(),
'ns': KeyInfo(), 'ns': KeyInfo(),
'regexp': KeyInfo(),
'srv-port': KeyInfo(), 'srv-port': KeyInfo(),
'srv-priority': KeyInfo(), 'srv-priority': KeyInfo(),
'srv-target': KeyInfo(), 'srv-target': KeyInfo(),

View file

@ -465,6 +465,24 @@ def polish_entry(entry, path_info, module, for_text):
for key, field_info in path_info.fields.items(): for key, field_info in path_info.fields.items():
if field_info.required and key not in entry: 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)) 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): def remove_irrelevant_data(entry, path_info):

View file

@ -54,6 +54,22 @@ def test_api_data_errors():
APIData(stratify_keys=['foo'], fields={}) APIData(stratify_keys=['foo'], fields={})
assert exc.value.args[0] == 'Stratify key foo must be in 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(): def test_key_info_errors():
values = [ values = [

View file

@ -385,9 +385,9 @@ class TestRouterosApiModifyModule(ModuleTestCase):
with self.assertRaises(AnsibleFailJson) as exc: with self.assertRaises(AnsibleFailJson) as exc:
args = self.config_module_args.copy() args = self.config_module_args.copy()
args.update({ args.update({
'path': 'ip dns static', 'path': 'ip dhcp-server',
'data': [{ 'data': [{
'address': '1.2.3.4', 'interface': 'eth0',
}], }],
}) })
set_module_args(args) set_module_args(args)
@ -397,6 +397,40 @@ class TestRouterosApiModifyModule(ModuleTestCase):
self.assertEqual(result['failed'], True) 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.') 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', @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)) new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
def test_sync_list_idempotent(self): def test_sync_list_idempotent(self):