Add version foot protector

python-novaclient is frozen and won't accept support for new
microversions. Attempting to use a microversion newer than what we
support is likely to break things is unexpected ways. Save people's feet
from this shotgun by introducing a new helper function, 'check_version',
that we use to ensure we actually support the version in question. We
rework the existing check_major_version to make it much faster since we
only care about v2 and have done so for a very long time.

Change-Id: I4d3fba6fcbf785ef3309b8f9eee45e31c7919777
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2026-02-27 18:47:55 +00:00
parent e00ed698af
commit 7d6ce9668e
3 changed files with 73 additions and 23 deletions

View File

@@ -14,7 +14,6 @@
import functools
import logging
import os
import pkgutil
import re
import traceback
import warnings
@@ -195,35 +194,59 @@ class VersionedMethod(object):
def get_available_major_versions():
# NOTE(andreykurilin): available clients version should not be
# hardcoded, so let's discover them.
matcher = re.compile(r"v[0-9]*$")
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
available_versions = [name[1:] for loader, name, ispkg in submodules
if matcher.search(name)]
return available_versions
return ['2']
def check_major_version(api_version):
"""Checks major part of ``APIVersion`` obj is supported.
:raises novaclient.exceptions.UnsupportedVersion: if major part is not
supported
supported
"""
available_versions = get_available_major_versions()
if (not api_version.is_null() and
str(api_version.ver_major) not in available_versions):
if len(available_versions) == 1:
msg = _("Invalid client version '%(version)s'. "
"Major part should be '%(major)s'") % {
"version": api_version.get_string(),
"major": available_versions[0]}
else:
msg = _("Invalid client version '%(version)s'. "
"Major part must be one of: '%(major)s'") % {
"version": api_version.get_string(),
"major": ", ".join(available_versions)}
if api_version.is_null():
return
if api_version.ver_major == 2:
return
msg = _(
"Invalid client version '%(version)s'. Major part should be '2'"
) % {"version": api_version.get_string()}
raise exceptions.UnsupportedVersion(msg)
def check_version(api_version):
"""Checks if version of ``APIVersion`` is supported.
Provided as an alternative to :func:`check_major_version` to avoid changing
the behavior of that function.
:raises novaclient.exceptions.UnsupportedVersion: if major part is not
supported
"""
if api_version.is_null():
return
# we can't use API_MIN_VERSION since we do support 2.0 (which is 2.1 but
# less strict)
if api_version < APIVersion('2.0'):
msg = _(
"Invalid client version '%(version)s'. "
"Min version supported is '%(min_version)s'"
) % {
"version": api_version.get_string(),
"min_version": novaclient.API_MIN_VERSION,
}
raise exceptions.UnsupportedVersion(msg)
if api_version > novaclient.API_MAX_VERSION:
msg = _(
"Invalid client version '%(version)s'. "
"Max version supported is '%(max_version)s'"
) % {
"version": api_version.get_string(),
"max_version": novaclient.API_MAX_VERSION,
}
raise exceptions.UnsupportedVersion(msg)

View File

@@ -51,6 +51,12 @@ class SessionClient(adapter.LegacyJsonAdapter):
self.timings = kwargs.pop('timings', False)
self.api_version = kwargs.pop('api_version', None)
self.api_version = self.api_version or api_versions.APIVersion()
if isinstance(self.api_version, str):
self.api_version = api_versions.APIVersion(self.api_version)
api_versions.check_version(self.api_version)
super(SessionClient, self).__init__(*args, **kwargs)
def request(self, url, method, **kwargs):

View File

@@ -343,6 +343,27 @@ class WrapsTestCase(utils.TestCase):
self.assertEqual(expected_name, fake_func.__id__)
class CheckVersionTestCase(utils.TestCase):
def test_version_unsupported(self):
for version in ('1.0', '1.5', '1.100'):
with self.subTest('version too old', version=version):
self.assertRaises(
exceptions.UnsupportedVersion,
api_versions.check_version,
api_versions.APIVersion(version))
for version in ('2.97', '2.101', '3.0'):
with self.subTest('version too new', version=version):
self.assertRaises(
exceptions.UnsupportedVersion,
api_versions.check_version,
api_versions.APIVersion(version))
for version in ('2.1', '2.57', '2.96'):
with self.subTest('version just right', version=version):
api_versions.check_version(api_versions.APIVersion(version))
class DiscoverVersionTestCase(utils.TestCase):
def setUp(self):
super(DiscoverVersionTestCase, self).setUp()