mirror of
https://github.com/ansible-collections/community.routeros.git
synced 2025-06-24 10:48:49 +02:00
Support absent values. Support absent value 'all' for 'server' in /ip dhcp-server lease. (#107)
This commit is contained in:
parent
a2ace3fb79
commit
f797b4a231
7 changed files with 232 additions and 10 deletions
2
changelogs/fragments/107-api-path-ip-dhcp-lease.yml
Normal file
2
changelogs/fragments/107-api-path-ip-dhcp-lease.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- "api_modify, api_info - make API path ``ip dhcp-server lease`` support ``server=all`` (https://github.com/ansible-collections/community.routeros/issues/104, https://github.com/ansible-collections/community.routeros/pull/107)."
|
|
@ -46,18 +46,21 @@ class APIData(object):
|
||||||
|
|
||||||
|
|
||||||
class KeyInfo(object):
|
class KeyInfo(object):
|
||||||
def __init__(self, _dummy=None, can_disable=False, remove_value=None, default=None, required=False, automatically_computed_from=None):
|
def __init__(self, _dummy=None, can_disable=False, remove_value=None, absent_value=None, default=None, required=False, automatically_computed_from=None):
|
||||||
if _dummy is not None:
|
if _dummy is not None:
|
||||||
raise ValueError('KeyInfo() does not have positional arguments')
|
raise ValueError('KeyInfo() does not have positional arguments')
|
||||||
if sum([required, default is not None, automatically_computed_from is not None, can_disable]) > 1:
|
if sum([required, default is not None, automatically_computed_from is not None, can_disable]) > 1:
|
||||||
raise ValueError('required, default, automatically_computed_from, and can_disable are mutually exclusive')
|
raise ValueError('required, default, automatically_computed_from, and can_disable are mutually exclusive')
|
||||||
if not can_disable and remove_value is not None:
|
if not can_disable and remove_value is not None:
|
||||||
raise ValueError('remove_value can only be specified if can_disable=True')
|
raise ValueError('remove_value can only be specified if can_disable=True')
|
||||||
|
if absent_value is not None and any([default is not None, automatically_computed_from is not None, can_disable]):
|
||||||
|
raise ValueError('absent_value can not be combined with default, automatically_computed_from, can_disable=True, or absent_value')
|
||||||
self.can_disable = can_disable
|
self.can_disable = can_disable
|
||||||
self.remove_value = remove_value
|
self.remove_value = remove_value
|
||||||
self.automatically_computed_from = automatically_computed_from
|
self.automatically_computed_from = automatically_computed_from
|
||||||
self.default = default
|
self.default = default
|
||||||
self.required = required
|
self.required = required
|
||||||
|
self.absent_value = absent_value
|
||||||
|
|
||||||
|
|
||||||
def split_path(path):
|
def split_path(path):
|
||||||
|
@ -755,7 +758,7 @@ PATHS = {
|
||||||
'disabled': KeyInfo(default=False),
|
'disabled': KeyInfo(default=False),
|
||||||
'insert-queue-before': KeyInfo(can_disable=True),
|
'insert-queue-before': KeyInfo(can_disable=True),
|
||||||
'mac-address': KeyInfo(can_disable=True, remove_value=''),
|
'mac-address': KeyInfo(can_disable=True, remove_value=''),
|
||||||
'server': KeyInfo(),
|
'server': KeyInfo(absent_value='all'),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
('ip', 'dhcp-server', 'network'): APIData(
|
('ip', 'dhcp-server', 'network'): APIData(
|
||||||
|
|
|
@ -275,10 +275,12 @@ def main():
|
||||||
if handle_disabled == 'exclamation':
|
if handle_disabled == 'exclamation':
|
||||||
k = '!%s' % k
|
k = '!%s' % k
|
||||||
entry[k] = None
|
entry[k] = None
|
||||||
if hide_defaults:
|
for k, field_info in path_info.fields.items():
|
||||||
for k, field_info in path_info.fields.items():
|
if hide_defaults:
|
||||||
if field_info.default is not None and entry.get(k) == field_info.default:
|
if field_info.default is not None and entry.get(k) == field_info.default:
|
||||||
entry.pop(k)
|
entry.pop(k)
|
||||||
|
if field_info.absent_value and k not in entry:
|
||||||
|
entry[k] = field_info.absent_value
|
||||||
result.append(entry)
|
result.append(entry)
|
||||||
|
|
||||||
module.exit_json(result=result)
|
module.exit_json(result=result)
|
||||||
|
|
|
@ -446,6 +446,15 @@ def remove_dynamic(entries):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_data(api_path, path_info):
|
||||||
|
entries = list(api_path)
|
||||||
|
for entry in entries:
|
||||||
|
for k, field_info in path_info.fields.items():
|
||||||
|
if field_info.absent_value is not None and k not in entry:
|
||||||
|
entry[k] = field_info.absent_value
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def sync_list(module, api, path, path_info):
|
def sync_list(module, api, path, path_info):
|
||||||
handle_absent_entries = module.params['handle_absent_entries']
|
handle_absent_entries = module.params['handle_absent_entries']
|
||||||
handle_entries_content = module.params['handle_entries_content']
|
handle_entries_content = module.params['handle_entries_content']
|
||||||
|
@ -476,7 +485,7 @@ def sync_list(module, api, path, path_info):
|
||||||
|
|
||||||
api_path = compose_api_path(api, path)
|
api_path = compose_api_path(api, path)
|
||||||
|
|
||||||
old_data = list(api_path)
|
old_data = get_api_data(api_path, path_info)
|
||||||
old_data = remove_dynamic(old_data)
|
old_data = remove_dynamic(old_data)
|
||||||
stratified_old_data = defaultdict(list)
|
stratified_old_data = defaultdict(list)
|
||||||
for index, entry in enumerate(old_data):
|
for index, entry in enumerate(old_data):
|
||||||
|
@ -588,7 +597,7 @@ def sync_list(module, api, path, path_info):
|
||||||
|
|
||||||
# For sake of completeness, retrieve the full new data:
|
# For sake of completeness, retrieve the full new data:
|
||||||
if modify_list or create_list or reorder_list:
|
if modify_list or create_list or reorder_list:
|
||||||
new_data = remove_dynamic(list(api_path))
|
new_data = remove_dynamic(get_api_data(api_path, path_info))
|
||||||
|
|
||||||
# Remove 'irrelevant' data
|
# Remove 'irrelevant' data
|
||||||
for entry in old_data:
|
for entry in old_data:
|
||||||
|
@ -656,7 +665,7 @@ def sync_with_primary_keys(module, api, path, path_info):
|
||||||
|
|
||||||
api_path = compose_api_path(api, path)
|
api_path = compose_api_path(api, path)
|
||||||
|
|
||||||
old_data = list(api_path)
|
old_data = get_api_data(api_path, path_info)
|
||||||
old_data = remove_dynamic(old_data)
|
old_data = remove_dynamic(old_data)
|
||||||
old_data_by_key = OrderedDict()
|
old_data_by_key = OrderedDict()
|
||||||
id_by_key = {}
|
id_by_key = {}
|
||||||
|
@ -782,7 +791,7 @@ def sync_with_primary_keys(module, api, path, path_info):
|
||||||
|
|
||||||
# For sake of completeness, retrieve the full new data:
|
# For sake of completeness, retrieve the full new data:
|
||||||
if modify_list or create_list or reorder_list:
|
if modify_list or create_list or reorder_list:
|
||||||
new_data = remove_dynamic(list(api_path))
|
new_data = remove_dynamic(get_api_data(api_path, path_info))
|
||||||
|
|
||||||
# Remove 'irrelevant' data
|
# Remove 'irrelevant' data
|
||||||
for entry in old_data:
|
for entry in old_data:
|
||||||
|
@ -818,7 +827,7 @@ def sync_single_value(module, api, path, path_info):
|
||||||
|
|
||||||
api_path = compose_api_path(api, path)
|
api_path = compose_api_path(api, path)
|
||||||
|
|
||||||
old_data = list(api_path)
|
old_data = get_api_data(api_path, path_info)
|
||||||
if len(old_data) != 1:
|
if len(old_data) != 1:
|
||||||
module.fail_json(
|
module.fail_json(
|
||||||
msg='Internal error: retrieving /{path} resulted in {count} elements. Expected exactly 1.'.format(
|
msg='Internal error: retrieving /{path} resulted in {count} elements. Expected exactly 1.'.format(
|
||||||
|
@ -839,7 +848,7 @@ def sync_single_value(module, api, path, path_info):
|
||||||
except (LibRouterosError, UnicodeEncodeError) as e:
|
except (LibRouterosError, UnicodeEncodeError) as e:
|
||||||
module.fail_json(msg='Error while modifying: {error}'.format(error=to_native(e)))
|
module.fail_json(msg='Error while modifying: {error}'.format(error=to_native(e)))
|
||||||
# Retrieve latest version
|
# Retrieve latest version
|
||||||
new_data = list(api_path)
|
new_data = get_api_data(api_path, path_info)
|
||||||
if len(new_data) == 1:
|
if len(new_data) == 1:
|
||||||
updated_entry = new_data[0]
|
updated_entry = new_data[0]
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,8 @@ def _normalize_entry(entry, path_info):
|
||||||
if ('!%s' % key) in entry:
|
if ('!%s' % key) in entry:
|
||||||
entry.pop(key, None)
|
entry.pop(key, None)
|
||||||
del entry['!%s' % key]
|
del entry['!%s' % key]
|
||||||
|
if data.absent_value is not None and key in entry and entry[key] == data.absent_value:
|
||||||
|
del entry[key]
|
||||||
|
|
||||||
|
|
||||||
def massage_expected_result_data(values, path, keep_all=False, remove_dynamic=False):
|
def massage_expected_result_data(values, path, keep_all=False, remove_dynamic=False):
|
||||||
|
@ -142,6 +144,9 @@ def massage_expected_result_data(values, path, keep_all=False, remove_dynamic=Fa
|
||||||
if key == '.id' or key in path_info.fields:
|
if key == '.id' or key in path_info.fields:
|
||||||
continue
|
continue
|
||||||
del entry[key]
|
del entry[key]
|
||||||
|
for key, data in path_info.fields.items():
|
||||||
|
if data.absent_value is not None and key not in entry:
|
||||||
|
entry[key] = data.absent_value
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -406,3 +406,92 @@ class TestRouterosApiInfoModule(ModuleTestCase):
|
||||||
'dynamic': True,
|
'dynamic': True,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
|
||||||
|
def test_absent(self, mock_compose_api_path):
|
||||||
|
mock_compose_api_path.return_value = [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'mac-address': '11:22:33:44:55:66',
|
||||||
|
'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
|
||||||
|
'address-lists': '',
|
||||||
|
'server': 'main',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'waiting',
|
||||||
|
'last-seen': 'never',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
'comment': 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*2',
|
||||||
|
'address': '192.168.88.3',
|
||||||
|
'mac-address': '11:22:33:44:55:77',
|
||||||
|
'client-id': '1:2:3:4:5:6:7',
|
||||||
|
'address-lists': '',
|
||||||
|
'server': 'main',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'bound',
|
||||||
|
'expires-after': '3d7m8s',
|
||||||
|
'last-seen': '1m52s',
|
||||||
|
'active-address': '192.168.88.14',
|
||||||
|
'active-mac-address': '11:22:33:44:55:76',
|
||||||
|
'active-client-id': '1:2:3:4:5:6:7',
|
||||||
|
'active-server': 'main',
|
||||||
|
'host-name': 'bar',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*3',
|
||||||
|
'address': '0.0.0.1',
|
||||||
|
'mac-address': '00:00:00:00:00:01',
|
||||||
|
'address-lists': '',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'waiting',
|
||||||
|
'last-seen': 'never',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dhcp-server lease',
|
||||||
|
'handle_disabled': 'omit',
|
||||||
|
})
|
||||||
|
set_module_args(args)
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
result = exc.exception.args[0]
|
||||||
|
self.assertEqual(result['changed'], False)
|
||||||
|
self.assertEqual(result['result'], [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'mac-address': '11:22:33:44:55:66',
|
||||||
|
'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
|
||||||
|
'server': 'main',
|
||||||
|
'comment': 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*2',
|
||||||
|
'address': '192.168.88.3',
|
||||||
|
'mac-address': '11:22:33:44:55:77',
|
||||||
|
'client-id': '1:2:3:4:5:6:7',
|
||||||
|
'server': 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*3',
|
||||||
|
'address': '0.0.0.1',
|
||||||
|
'mac-address': '00:00:00:00:00:01',
|
||||||
|
'server': 'all',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
|
@ -93,6 +93,74 @@ START_IP_ADDRESS = [
|
||||||
|
|
||||||
START_IP_ADDRESS_OLD_DATA = massage_expected_result_data(START_IP_ADDRESS, ('ip', 'address'))
|
START_IP_ADDRESS_OLD_DATA = massage_expected_result_data(START_IP_ADDRESS, ('ip', 'address'))
|
||||||
|
|
||||||
|
START_IP_DHCP_SEVER_LEASE = [
|
||||||
|
{
|
||||||
|
'.id': '*1',
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'mac-address': '11:22:33:44:55:66',
|
||||||
|
'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
|
||||||
|
'address-lists': '',
|
||||||
|
'server': 'main',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'waiting',
|
||||||
|
'last-seen': 'never',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
'comment': 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*2',
|
||||||
|
'address': '192.168.88.3',
|
||||||
|
'mac-address': '11:22:33:44:55:77',
|
||||||
|
'client-id': '1:2:3:4:5:6:7',
|
||||||
|
'address-lists': '',
|
||||||
|
'server': 'main',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'bound',
|
||||||
|
'expires-after': '3d7m8s',
|
||||||
|
'last-seen': '1m52s',
|
||||||
|
'active-address': '192.168.88.14',
|
||||||
|
'active-mac-address': '11:22:33:44:55:76',
|
||||||
|
'active-client-id': '1:2:3:4:5:6:7',
|
||||||
|
'active-server': 'main',
|
||||||
|
'host-name': 'bar',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*3',
|
||||||
|
'address': '0.0.0.1',
|
||||||
|
'mac-address': '00:00:00:00:00:01',
|
||||||
|
'address-lists': '',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'waiting',
|
||||||
|
'last-seen': 'never',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'.id': '*4',
|
||||||
|
'address': '0.0.0.2',
|
||||||
|
'mac-address': '00:00:00:00:00:02',
|
||||||
|
'address-lists': '',
|
||||||
|
'dhcp-option': '',
|
||||||
|
'status': 'waiting',
|
||||||
|
'last-seen': 'never',
|
||||||
|
'radius': False,
|
||||||
|
'dynamic': False,
|
||||||
|
'blocked': False,
|
||||||
|
'disabled': False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
START_IP_DHCP_SEVER_LEASE_OLD_DATA = massage_expected_result_data(START_IP_DHCP_SEVER_LEASE, ('ip', 'dhcp-server', 'lease'))
|
||||||
|
|
||||||
|
|
||||||
class TestRouterosApiModifyModule(ModuleTestCase):
|
class TestRouterosApiModifyModule(ModuleTestCase):
|
||||||
|
|
||||||
|
@ -1538,3 +1606,47 @@ class TestRouterosApiModifyModule(ModuleTestCase):
|
||||||
'disabled': False,
|
'disabled': False,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
|
||||||
|
new=create_fake_path(('ip', 'dhcp-server', 'lease'), START_IP_DHCP_SEVER_LEASE, read_only=True))
|
||||||
|
def test_absent_value(self):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exc:
|
||||||
|
args = self.config_module_args.copy()
|
||||||
|
args.update({
|
||||||
|
'path': 'ip dhcp-server lease',
|
||||||
|
'data': [
|
||||||
|
{
|
||||||
|
'address': '192.168.88.2',
|
||||||
|
'mac-address': '11:22:33:44:55:66',
|
||||||
|
'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
|
||||||
|
'server': 'main',
|
||||||
|
'comment': 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'address': '192.168.88.3',
|
||||||
|
'mac-address': '11:22:33:44:55:77',
|
||||||
|
'client-id': '1:2:3:4:5:6:7',
|
||||||
|
'server': 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'address': '0.0.0.1',
|
||||||
|
'mac-address': '00:00:00:00:00:01',
|
||||||
|
'server': 'all',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'address': '0.0.0.2',
|
||||||
|
'mac-address': '00:00:00:00:00:02',
|
||||||
|
'server': 'all',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'handle_absent_entries': 'remove',
|
||||||
|
'handle_entries_content': 'remove',
|
||||||
|
'ensure_order': 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_DHCP_SEVER_LEASE_OLD_DATA)
|
||||||
|
self.assertEqual(result['new_data'], START_IP_DHCP_SEVER_LEASE_OLD_DATA)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue