Allow to differ on API paths based on RouterOS version (2/2) (#212)

* Allow to add versioned field for paths.

* The field added in 1aa41ad375 is RouterOS 7.7+.

* The fields added in 2e1159b4c4 are RouterOS 7.5+.
This commit is contained in:
Felix Fontein 2023-09-01 23:17:47 +02:00 committed by GitHub
parent 4b0995135c
commit dcc1cf441d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 17 deletions

View file

@ -37,7 +37,7 @@ class APIData(object):
self.unversioned = unversioned self.unversioned = unversioned
self.versioned = versioned self.versioned = versioned
if self.unversioned is not None: if self.unversioned is not None:
self.needs_version = False self.needs_version = self.unversioned.needs_version
self.fully_understood = self.unversioned.fully_understood self.fully_understood = self.unversioned.fully_understood
else: else:
self.needs_version = self.versioned is not None self.needs_version = self.versioned is not None
@ -47,30 +47,35 @@ class APIData(object):
if unversioned.fully_understood: if unversioned.fully_understood:
self.fully_understood = True self.fully_understood = True
break break
self._current = None if self.needs_version else self.unversioned
def provide_version(self, version): def provide_version(self, version):
if not self.needs_version: if not self.needs_version:
return self.unversioned.fully_understood return self.unversioned.fully_understood
api_version = LooseVersion(version) api_version = LooseVersion(version)
for other_version, comparator, unversioned in self.versioned: if self.unversioned is not None:
self._current = self.unversioned.specialize_for_version(api_version)
return self._current.fully_understood
for other_version, comparator, data in self.versioned:
if other_version == '*' and comparator == '*': if other_version == '*' and comparator == '*':
self.unversioned = unversioned self._current = data.specialize_for_version(api_version)
return self.unversioned.fully_supported return self._current.fully_supported
other_api_version = LooseVersion(other_version) other_api_version = LooseVersion(other_version)
if _compare(api_version, other_api_version, comparator): if _compare(api_version, other_api_version, comparator):
self.unversioned = unversioned self._current = data.specialize_for_version(api_version)
return self.unversioned.fully_supported return self._current.fully_supported
self.unversioned = None self._current = None
return False return False
def get_data(self): def get_data(self):
if self.unversioned is None: if self._current is None:
raise ValueError('either provide_version() was not called or it returned False') raise ValueError('either provide_version() was not called or it returned False')
return self.unversioned return self._current
class VersionedAPIData(object): class VersionedAPIData(object):
def __init__(self, primary_keys=None, def __init__(self,
primary_keys=None,
stratify_keys=None, stratify_keys=None,
required_one_of=None, required_one_of=None,
mutually_exclusive=None, mutually_exclusive=None,
@ -79,7 +84,8 @@ class VersionedAPIData(object):
unknown_mechanism=False, unknown_mechanism=False,
fully_understood=False, fully_understood=False,
fixed_entries=False, fixed_entries=False,
fields=None): fields=None,
versioned_fields=None):
if sum([primary_keys is not None, stratify_keys is not None, has_identifier, single_value, unknown_mechanism]) > 1: if sum([primary_keys is not None, stratify_keys is not None, has_identifier, single_value, unknown_mechanism]) > 1:
raise ValueError('primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive') raise ValueError('primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive')
if unknown_mechanism and fully_understood: if unknown_mechanism and fully_understood:
@ -98,6 +104,17 @@ class VersionedAPIData(object):
if fields is None: if fields is None:
raise ValueError('fields must be provided') raise ValueError('fields must be provided')
self.fields = fields self.fields = fields
if versioned_fields is not None:
if not isinstance(versioned_fields, list):
raise ValueError('unversioned_fields must be a list')
for conditions, name, field in versioned_fields:
if not isinstance(conditions, (tuple, list)):
raise ValueError('conditions must be a list or tuple')
if not isinstance(field, KeyInfo):
raise ValueError('field must be a KeyInfo object')
if name in fields:
raise ValueError('"{name}" appears both in fields and versioned_fields'.format(name=name))
self.versioned_fields = versioned_fields or []
if primary_keys: if primary_keys:
for pk in primary_keys: for pk in primary_keys:
if pk not in fields: if pk not in fields:
@ -120,6 +137,35 @@ class VersionedAPIData(object):
for ek in exclusive_list: for ek in exclusive_list:
if ek not in fields: if ek not in fields:
raise ValueError('Mutually exclusive key {ek} must be in fields!'.format(ek=ek)) raise ValueError('Mutually exclusive key {ek} must be in fields!'.format(ek=ek))
self.needs_version = len(self.versioned_fields) > 0
def specialize_for_version(self, api_version):
fields = self.fields.copy()
for conditions, name, field in self.versioned_fields:
matching = True
for other_version, comparator in conditions:
other_api_version = LooseVersion(other_version)
if not _compare(api_version, other_api_version, comparator):
matching = False
break
if matching:
if name in fields:
raise ValueError(
'Internal error: field "{field}" already exists for {version}'.format(field=name, version=api_version)
)
fields[name] = field
return VersionedAPIData(
primary_keys=self.primary_keys,
stratify_keys=self.stratify_keys,
required_one_of=self.required_one_of,
mutually_exclusive=self.mutually_exclusive,
has_identifier=self.has_identifier,
single_value=self.single_value,
unknown_mechanism=self.unknown_mechanism,
fully_understood=self.fully_understood,
fixed_entries=self.fixed_entries,
fields=fields,
)
class KeyInfo(object): class KeyInfo(object):
@ -1241,10 +1287,12 @@ PATHS = {
unversioned=VersionedAPIData( unversioned=VersionedAPIData(
single_value=True, single_value=True,
fully_understood=True, fully_understood=True,
versioned_fields=[
([('7.7', '>=')], 'mode', KeyInfo(default='tx-and-rx')),
],
fields={ fields={
'discover-interface-list': KeyInfo(), 'discover-interface-list': KeyInfo(),
'lldp-med-net-policy-vlan': KeyInfo(default='disabled'), 'lldp-med-net-policy-vlan': KeyInfo(default='disabled'),
'mode': KeyInfo(default='tx-and-rx'),
'protocol': KeyInfo(default='cdp,lldp,mndp'), 'protocol': KeyInfo(default='cdp,lldp,mndp'),
}, },
), ),
@ -1797,14 +1845,16 @@ PATHS = {
fully_understood=True, fully_understood=True,
required_one_of=[['name', 'regexp']], required_one_of=[['name', 'regexp']],
mutually_exclusive=[['name', 'regexp']], mutually_exclusive=[['name', 'regexp']],
versioned_fields=[
([('7.5', '>=')], 'address-list', KeyInfo()),
([('7.5', '>=')], 'match-subdomain', KeyInfo(default=False)),
],
fields={ fields={
'address': KeyInfo(), 'address': KeyInfo(),
'address-list': KeyInfo(),
'cname': KeyInfo(), 'cname': KeyInfo(),
'comment': KeyInfo(can_disable=True, remove_value=''), 'comment': KeyInfo(can_disable=True, remove_value=''),
'disabled': KeyInfo(default=False), 'disabled': KeyInfo(default=False),
'forward-to': KeyInfo(), 'forward-to': KeyInfo(),
'match-subdomain': KeyInfo(default=False),
'mx-exchange': KeyInfo(), 'mx-exchange': KeyInfo(),
'mx-preference': KeyInfo(), 'mx-preference': KeyInfo(),
'name': KeyInfo(), 'name': KeyInfo(),

View file

@ -9,7 +9,7 @@ __metaclass__ = type
from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS
FAKE_ROS_VERSION = '7.0.0' FAKE_ROS_VERSION = '7.5.0'
class FakeLibRouterosError(Exception): class FakeLibRouterosError(Exception):

View file

@ -7,7 +7,9 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import (
FAKE_ROS_VERSION, FakeLibRouterosError, Key, fake_ros_api,
)
from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
from ansible_collections.community.routeros.plugins.modules import api_info from ansible_collections.community.routeros.plugins.modules import api_info
@ -22,6 +24,10 @@ class TestRouterosApiInfoModule(ModuleTestCase):
self.module.check_has_library = MagicMock() self.module.check_has_library = MagicMock()
self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_info.create_api', MagicMock(new=fake_ros_api)) self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_info.create_api', MagicMock(new=fake_ros_api))
self.patch_create_api.start() self.patch_create_api.start()
self.patch_get_api_version = patch(
'ansible_collections.community.routeros.plugins.modules.api_info.get_api_version',
MagicMock(return_value=FAKE_ROS_VERSION))
self.patch_get_api_version.start()
self.module.Key = MagicMock(new=Key) self.module.Key = MagicMock(new=Key)
self.config_module_args = { self.config_module_args = {
'username': 'admin', 'username': 'admin',
@ -30,6 +36,7 @@ class TestRouterosApiInfoModule(ModuleTestCase):
} }
def tearDown(self): def tearDown(self):
self.patch_get_api_version.stop()
self.patch_create_api.stop() self.patch_create_api.stop()
def test_module_fail_when_required_args_missing(self): def test_module_fail_when_required_args_missing(self):

View file

@ -8,7 +8,7 @@ __metaclass__ = type
from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import ( from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import (
FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path, FAKE_ROS_VERSION, FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path,
) )
from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
from ansible_collections.community.routeros.plugins.modules import api_modify from ansible_collections.community.routeros.plugins.modules import api_modify
@ -302,6 +302,10 @@ class TestRouterosApiModifyModule(ModuleTestCase):
'ansible_collections.community.routeros.plugins.modules.api_modify.create_api', 'ansible_collections.community.routeros.plugins.modules.api_modify.create_api',
MagicMock(new=fake_ros_api)) MagicMock(new=fake_ros_api))
self.patch_create_api.start() self.patch_create_api.start()
self.patch_get_api_version = patch(
'ansible_collections.community.routeros.plugins.modules.api_modify.get_api_version',
MagicMock(return_value=FAKE_ROS_VERSION))
self.patch_get_api_version.start()
self.config_module_args = { self.config_module_args = {
'username': 'admin', 'username': 'admin',
'password': 'pаss', 'password': 'pаss',
@ -309,6 +313,7 @@ class TestRouterosApiModifyModule(ModuleTestCase):
} }
def tearDown(self): def tearDown(self):
self.patch_get_api_version.stop()
self.patch_create_api.stop() self.patch_create_api.stop()
def test_module_fail_when_required_args_missing(self): def test_module_fail_when_required_args_missing(self):