Support absent values. Support absent value 'all' for 'server' in /ip dhcp-server lease. (#107)

This commit is contained in:
Felix Fontein 2022-08-13 10:55:37 +02:00 committed by GitHub
parent a2ace3fb79
commit f797b4a231
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 232 additions and 10 deletions

View 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)."

View file

@ -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(

View file

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

View file

@ -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]

View file

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

View file

@ -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',
},
])

View file

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