Support same pkg version for multiple releases

It is possible for the version of a package to remain the same
across multiple releases of openstack so this adds support to
get_os_codename_package() to allow this.

If releases A, B and C have version N and we are on release B
this will return release C so that it does not look like there
is an upgrade necessary. The main change here that allows this
is to support matching major and minor version up to X.Y.Z as
opposed to previously only matching the major version X.

Related-Bug: #1973303

Change-Id: I138f61312efb728544276483b1a459b9eeecafdb
(cherry picked from commit 55f98e80e4)
This commit is contained in:
Edward Hope-Morley 2024-04-23 14:47:08 +01:00
parent 018b72d734
commit b956245392
2 changed files with 117 additions and 8 deletions

View File

@ -1156,13 +1156,48 @@ class BaseOpenStackCharmActions(object):
# x.y match only for 20XX.X
# and ignore patch level for other packages
match = re.match(r'^(\d+)\.(\d+)', vers)
match = re.match(r'^(20\d+)\.(\d+)', vers)
if match:
vers = match.group(0)
return vers
def get_closest_release_match(self, package_version, codenames):
"""
Multiple releases can share the same version of the package so this
collects contiguous versions, maintaining order then picks the most
recent version that is less than or equal to what is installed.
:param package_version: version of installed package.
:type package_version: str
:param codenames: OrderedDict of version prefixes and release names.
:type codenames: OrderedDict
:returns: tuple of release name and version or None if match was not
found.
:rtype: tuple(version, release_name)
"""
hookenv.log('getting rel name for pkg ver={} from codenames={}'.
format(package_version, codenames), level=hookenv.DEBUG)
reversed_codenames = collections.OrderedDict(
reversed(codenames.items()))
splitver = package_version.split('.')
# Starting from the leading dot and going backwards, we see if a
# release name matches and pick the first we find. If the same release
# name is registered for more than one version, we pick the highest
# release to indicate that the version has no release upgrade
# available.
for dotpos in reversed(range(min(len(splitver), 3))):
ver_pfix = '.'.join(splitver[:dotpos + 1])
# Find, in descending order, the first closest match.
for ver, rname in reversed_codenames.items():
if not ver.startswith(ver_pfix):
continue
if fetch.apt_pkg.version_compare(ver, package_version) <= 0:
return (ver, rname)
def get_os_codename_package(self, package, codenames, fatal=True,
apt_cache_sufficient=False):
"""Derive OpenStack release codename from a package.
@ -1220,20 +1255,22 @@ class BaseOpenStackCharmActions(object):
if codename:
return codename
vers = self.get_package_version(
package_version = self.get_package_version(
package,
apt_cache_sufficient=apt_cache_sufficient)
# Generate a major version number for newer semantic
# versions of openstack projects
major_vers = vers.split('.')[0]
except Exception:
if fatal:
raise
else:
return None
if (package in codenames and
major_vers in codenames[package]):
return codenames[package][major_vers]
if package not in codenames:
return
vr_match = self.get_closest_release_match(package_version,
codenames[package])
if vr_match:
return vr_match[1]
def get_os_version_snap(self, snap, fatal=True):
"""Derive OpenStack version number from an installed snap.

View File

@ -885,6 +885,76 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest):
for call in self.render.call_args_list:
self.assertTrue(call[1]['context'])
def test_get_closest_release_match(self):
# this is mocked universally in unit_tests/__init__.py so we have
# to apply to not so great mock on top to allow the method to be
# called.
def fake_version_compare(a, b):
if a > b:
return 1
elif a < b:
return -1
return 0
self.patch_object(chm_core.charmhelpers.fetch.apt_pkg,
'version_compare')
chm_core.charmhelpers.fetch.apt_pkg.version_compare.side_effect = \
fake_version_compare
codenames = collections.OrderedDict([('3.9', 'ussuri'),
('4.0', 'victoria'),
('4.0.1', 'yoga')])
pkg_ver = '4'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.1', pkg_ver), mock.call('4.0', pkg_ver)])
self.assertEqual(release, None)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
pkg_ver = '4.0'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.1', pkg_ver), mock.call('4.0', pkg_ver)])
self.assertEqual(release, ('4.0', 'victoria'))
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
pkg_ver = '4.0.1'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.1', pkg_ver)])
self.assertEqual(release, ('4.0.1', 'yoga'))
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
pkg_ver = '4.0.2'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.1', pkg_ver)])
self.assertEqual(release, ('4.0.1', 'yoga'))
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
codenames['4.0.3'] = 'antelope'
pkg_ver = '4.0.2'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.3', pkg_ver), mock.call('4.0.1', pkg_ver)])
self.assertEqual(release, ('4.0.1', 'yoga'))
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
release = self.target.get_closest_release_match('4.0.1.1', codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.1', '4.0.1.1')])
self.assertEqual(release, ('4.0.1', 'yoga'))
chm_core.charmhelpers.fetch.apt_pkg.version_compare.reset_mock()
pkg_ver = '4.4.1+git2022033113.2339b9e9-0ubuntu1'
release = self.target.get_closest_release_match(pkg_ver, codenames)
chm_core.charmhelpers.fetch.apt_pkg.version_compare.assert_has_calls([
mock.call('4.0.3', pkg_ver)])
self.assertEqual(release, ('4.0.3', 'antelope'))
def test_get_os_codename_package(self):
codenames = {
'testpkg': collections.OrderedDict([
@ -902,6 +972,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest):
self.patch_object(chm_core.os_utils, 'get_installed_os_version')
self.get_installed_os_version.return_value = None
self.upstream_version.return_value = '3.0.0~b1'
self.patch_object(self.target, 'get_closest_release_match')
self.get_closest_release_match.return_value = (3, 'newton')
self.patch_object(self.target, 'configure_source')
self.configure_source.side_effect = KeyError
self.assertEqual(