diff --git a/charms_openstack/plugins/trilio.py b/charms_openstack/plugins/trilio.py index 285ca14..43507dc 100644 --- a/charms_openstack/plugins/trilio.py +++ b/charms_openstack/plugins/trilio.py @@ -15,16 +15,85 @@ import base64 import os +import re +from urllib.parse import urlparse + +import charms_openstack.adapters import charms_openstack.charm import charmhelpers.core as ch_core import charmhelpers.fetch as fetch +import charmhelpers.core.unitdata as unitdata +import charmhelpers.contrib.openstack.utils as os_utils import charms.reactive as reactive TV_MOUNTS = "/var/triliovault-mounts" +# Used to store the discovered release version for caching between invocations +TRILIO_RELEASE_KEY = 'charmers.trilio-release-version' + +# _trilio_releases{} is a dictionary of release -> class that is instantiated +# according to the release that is being requested. i.e. a charm can +# handle more than one release. The BaseOpenStackCharm() derived class sets the +# `release` variable to indicate which OpenStack release that the charm +# supports # and `trilio_release` to indicate which Trilio release the charm +# supports. # Any subsequent releases that need a different/specialised charm +# uses the # `release` and `trilio_release` class properties to indicate that +# it handles those releases onwards. +_trilio_releases = {} + + +@charms_openstack.adapters.config_property +def trilio_properties(cls): + """Trilio properties additions for config adapter. + + :param cls: Configuration Adapter class + :type cls: charms_openstack.adapters.DefaultConfigurationAdapter + """ + cur_ver = cls.charm_instance.release_pkg_version() + comp = fetch.apt_pkg.version_compare(cur_ver, '4.1') + if comp >= 0: + return { + 'db_type': 'dedicated', + 'transport_type': 'dmapi'} + else: + return { + 'db_type': 'legacy', + 'transport_type': 'legacy'} + + +class AptPkgVersion(): + """Allow package version to be compared.""" + + def __init__(self, version): + self.version = version + + def __lt__(self, other): + return fetch.apt_pkg.version_compare(self.version, other.version) == -1 + + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + def __gt__(self, other): + return fetch.apt_pkg.version_compare(self.version, other.version) == 1 + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __eq__(self, other): + return fetch.apt_pkg.version_compare(self.version, other.version) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return self.version + + def __hash__(self): + return hash(repr(self)) + class NFSShareNotMountedException(Exception): """Signal that the trilio nfs share is not mount""" @@ -85,7 +154,248 @@ def _install_triliovault(charm): reactive.clear_flag('upgrade.triliovault') -class TrilioVaultCharm(charms_openstack.charm.HAOpenStackCharm): +def get_trilio_codename_install_source(trilio_source): + """Derive codename from trilio source string. + + Try and derive a trilio version from a deb string like: + 'deb [trusted=yes] https://apt.fury.io/triliodata-4-0/ /' + + :param trilio_source: Trilio source + :type trilio_source: str + :returns: Trilio version + :rtype: str + :raises: AssertionError + """ + deb_url = trilio_source.split()[-2] + code = re.findall(r'-(\d*-\d*)', urlparse(deb_url).path) + assert len(code) == 1, "Cannot derive release from {}".format(deb_url) + new_os_rel = code[0].replace('-', '.') + return new_os_rel + + +def make_trilio_get_charm_instance_handler(): + """This handler sets the get_charm_instance function. + """ + + @charms_openstack.charm.core.register_get_charm_instance + def get_trilio_charm_instance(release=None, package_type='deb', *args, + **kwargs): + """Get an instance of the charm based on the release (or use the + default if release is None). + + Note that it passes args and kwargs to the class __init__() method. + + :param release: String representing release wanted. Should be of the + form '_' + eg 'queens_4.0' + :type release: str + :param package_type: The package type required + :type package_type: str + :returns: Charm class + :rtype: BaseOpenStackCharm() derived class according to cls.releases + """ + cls = None + known_os_releases = sorted(_trilio_releases.keys()) + if release is None: + # If release is None then select the class(es) which supports the + # most recent OpenStack release, from within this set select the + # class that supports the most recent Trilio release. + os_release = known_os_releases[-1] + known_trilio_releases = sorted(_trilio_releases[os_release].keys()) + trilio_release = known_trilio_releases[-1] + cls = _trilio_releases[os_release][trilio_release][package_type] + else: + os_release, trilio_release = release.split('_') + trilio_release = AptPkgVersion(trilio_release) + if os_release not in os_utils.OPENSTACK_RELEASES: + raise RuntimeError( + "Release {} is not a known OpenStack release?".format( + os_release)) + os_release_index = os_utils.OPENSTACK_RELEASES.index(os_release) + if (os_release_index < + os_utils.OPENSTACK_RELEASES.index(known_os_releases[0])): + raise RuntimeError( + "Release {} is not supported by this charm. Earliest " + "support is {} release".format( + os_release, + known_os_releases[0])) + else: + known_trilio_releases = [] + # Search through the dictionary of registered charm classes + # looking for the most recent group which can support + # `os_release` + for known_os_release in reversed(known_os_releases): + _idx = os_utils.OPENSTACK_RELEASES.index(known_os_release) + if os_release_index >= _idx: + trilio_classes = _trilio_releases[known_os_release] + known_trilio_releases = sorted(trilio_classes.keys()) + break + # Search through the dictionary of registered charm classes + # that support `known_os_release` onwards and look for the + # class # which supports the most recent trilio release which + # is <= `trilio_release` + for known_trilio_release in reversed(known_trilio_releases): + if known_trilio_release <= trilio_release: + cls = trilio_classes[known_trilio_release][ + package_type] + # Found a class so exit loop + break + if cls is None: + raise RuntimeError("Release {} is not supported".format(release)) + return cls(release=os_release, *args, **kwargs) + + +def make_trilio_handlers(): + """This handler sets the trilio release selector get_charm_instance funcs. + """ + make_trilio_get_charm_instance_handler() + make_trilio_select_release_handler() + + +def make_trilio_select_release_handler(): + """This handler sets the release selector function. + """ + + @charms_openstack.charm.core.register_os_release_selector + def select_trilio_release(): + """Determine the OpenStack and Trilio release + + Determine the OpenStack release based on the `singleton.os_release_pkg` + that is installed. If it is not installed look for and exanine other + semantic versioned packages. If both those tactics fail fall back to + checking the charm `openstack-origin` option. + + Determine the Trilio release based on the `singleton.version_package` + that is installed. If it is not installed fall back to checking the + charm `triliovault-pkg-source` option. + + Note that this function caches the release after the first install so + that it doesn't need to keep going and getting it from the package + information. + """ + + singleton = None + # Search for target OpenStack Release + os_release_version = unitdata.kv().get( + charms_openstack.charm.core.OPENSTACK_RELEASE_KEY, + None) + if os_release_version is None: + try: + # First make an attempt of determining release from a charm + # instance defined package codename dictionary. + singleton = charms_openstack.charm.core.get_charm_instance() + if singleton.release_pkg is None: + raise RuntimeError("release_pkg is not set") + os_release_version = singleton.get_os_codename_package( + singleton.os_release_pkg, singleton.package_codenames, + apt_cache_sufficient=(not singleton.source_config_key)) + if os_release_version is None: + # Surprisingly get_os_codename_package called with + # ``Fatal=True`` does not raise an error when the charm + # class ``package_codenames`` map does not contain package + # or major version. We'll handle it here instead of + # changing the API of the method. + raise ValueError + except (AttributeError, ValueError): + try: + pkgs = os_utils.get_installed_semantic_versioned_packages() + pkg = pkgs[0] + except IndexError: + # A non-existent package will cause os_release to try other + # tactics for deriving the release. + pkg = 'dummy-package' + os_release_version = os_utils.os_release( + pkg, source_key=singleton.source_config_key) + unitdata.kv().set( + charms_openstack.charm.core.OPENSTACK_RELEASE_KEY, + os_release_version) + unitdata.kv().flush() + + # Search for target Trilio Release + trilio_release_version = unitdata.kv().get(TRILIO_RELEASE_KEY, None) + if trilio_release_version is None: + if not singleton: + singleton = charms_openstack.charm.core.get_charm_instance() + if singleton.version_package is None: + raise RuntimeError("version_package is not set") + try: + trilio_release_version = singleton.get_package_version( + singleton.version_package) + except (AttributeError, ValueError): + trilio_release_version = get_trilio_codename_install_source( + singleton.trilio_source) + unitdata.kv().set(TRILIO_RELEASE_KEY, trilio_release_version) + unitdata.kv().flush() + + return '{}_{}'.format(os_release_version, trilio_release_version) + + +class BaseTrilioCharmMeta(charms_openstack.charm.core.BaseOpenStackCharmMeta): + """Metaclass to handle registering charm classes by their supported + OpenStack release, Trilio release and package typea + + _trilio_releases has the form:: + + { + 'Openstack Code Name': { + 'Trilio Package Veersion': { + 'Package Type': }}, + """ + + def __init__(cls, name, mro, members): + """Receive the BaseOpenStackCharm() (derived) class and store the + release that it works against. Each class defines a 'release' which + corresponds to the Openstack release that it handles. The class should + also specify 'trilio_release' which defines the Trilio releases it can + handle. + + :param name: string for class name. + :param mro: tuple of base classes. + :param members: dictionary of name to class attribute (f, p, a, etc.) + """ + # Do not attempt to calculate the release for an abstract class + if members.get('abstract_class', False): + return + if all(key in members.keys() for key in ['release', 'trilio_release']): + package_type = members.get('package_type', 'deb') + if package_type not in ('deb', 'snap'): + raise RuntimeError( + "Package type {} is not a known type" + .format(package_type)) + release = members['release'] + trilio_release = AptPkgVersion(members['trilio_release']) + if release not in os_utils.OPENSTACK_RELEASES: + raise RuntimeError( + "Release {} is not a known OpenStack release" + .format(release)) + try: + _pre = _trilio_releases[release][trilio_release][package_type] + except KeyError: + # All good this comination has not been registered yet. + pass + else: + raise RuntimeError( + "Release {} + {} defined more than once in classes {} and " + "{} (at least)" + .format(release, + trilio_release, + _pre.__name__, + name)) + # store the class against the release. + if release not in _trilio_releases: + _trilio_releases[release] = {} + if trilio_release not in _trilio_releases[release]: + _trilio_releases[release][trilio_release] = {} + _trilio_releases[release][trilio_release][package_type] = cls + else: + raise RuntimeError( + "class '{}' must define both the release it supports using " + "the 'release' class property and the trilio release it " + "supports using the 'trilio_release' class property.".format( + name)) + + +class TrilioVaultCharmMixin(): """The TrilioVaultCharm class provides common specialisation of certain functions for the Trilio charm set and is designed for use alongside other base charms.openstack classes @@ -94,7 +404,11 @@ class TrilioVaultCharm(charms_openstack.charm.HAOpenStackCharm): abstract_class = True def __init__(self, **kwargs): - super(TrilioVaultCharm, self).__init__(**kwargs) + try: + del kwargs['trilio_release'] + except KeyError: + pass + super().__init__(**kwargs) def configure_source(self): """Configure triliovault specific package sources in addition to @@ -114,17 +428,112 @@ class TrilioVaultCharm(charms_openstack.charm.HAOpenStackCharm): super().series_upgrade_complete() self.configure_source() + @property + def trilio_source(self): + """Trilio source config option""" + return self.config.get("triliovault-pkg-source") -class TrilioVaultSubordinateCharm(charms_openstack.charm.OpenStackCharm): - """The TrilioVaultSubordinateCharm class provides common specialisation - of certain functions for the Trilio charm set and is designed for use - alongside other base charms.openstack classes for subordinate charms - """ + def do_trilio_pkg_upgrade(self): + """Upgrade Trilio packages + """ + new_os_rel = get_trilio_codename_install_source( + self.trilio_source) + ch_core.hookenv.log('Performing Trilio upgrade to %s.' % (new_os_rel)) + + dpkg_opts = [ + '--option', 'Dpkg::Options::=--force-confnew', + '--option', 'Dpkg::Options::=--force-confdef', + ] + fetch.apt_update() + fetch.apt_install( + packages=self.all_packages, + options=dpkg_opts, + fatal=True) + self.remove_obsolete_packages() + + def do_trilio_upgrade_db_migration(self): + """Run Trilio DB sync + + Trilio charms sync_cmd refers to a trilio db sync. + + """ + super().do_openstack_upgrade_db_migration() + + def run_trilio_upgrade(self, interfaces_list=None): + """ + :param interfaces_list: List of instances of interface classes + :returns: None + """ + ch_core.hookenv.status_set('maintenance', 'Running openstack upgrade') + cur_os_release = self.get_os_codename_package( + self.os_release_pkg, + self.package_codenames) + new_trilio_release = get_trilio_codename_install_source( + self.trilio_source) + new_release = '{}_{}'.format(cur_os_release, new_trilio_release) + unitdata.kv().set(TRILIO_RELEASE_KEY, new_trilio_release) + _configure_triliovault_source() + target_charm = charms_openstack.charm.core.get_charm_instance( + new_release) + target_charm.do_trilio_pkg_upgrade() + target_charm.render_with_interfaces(interfaces_list) + target_charm.do_trilio_upgrade_db_migration() + + def trilio_upgrade_available(self, package=None): + """Check if an OpenStack upgrade is available + + :param package: str Package name to use to check upgrade availability + :returns: bool + """ + cur_vers = self.get_package_version(package) + avail_vers = get_trilio_codename_install_source( + self.trilio_source) + return fetch.apt_pkg.version_compare(avail_vers, cur_vers) == 1 + + def upgrade_if_available(self, interfaces_list): + if self.openstack_upgrade_available(self.os_release_pkg): + if self.config.get('action-managed-upgrade', False): + ch_core.hookenv.log('Not performing OpenStack upgrade as ' + 'action-managed-upgrade is enabled') + else: + self.run_upgrade(interfaces_list=interfaces_list) + if self.trilio_upgrade_available( + package=self.trilio_version_package()): + if self.config.get('action-managed-upgrade', False): + ch_core.hookenv.log('Not performing Trilio upgrade as ' + 'action-managed-upgrade is enabled') + else: + self.run_trilio_upgrade(interfaces_list=interfaces_list) + + @classmethod + def trilio_version_package(cls): + raise NotImplementedError + + @property + def version_package(self): + return self.trilio_version_package() + + @property + def release_pkg(self): + return self.trilio_version_package() + + @classmethod + def release_pkg_version(cls): + return cls.get_package_version(cls.trilio_version_package()) + + +class TrilioVaultCharm(TrilioVaultCharmMixin, + charms_openstack.charm.HAOpenStackCharm, + metaclass=BaseTrilioCharmMeta): abstract_class = True - def __init__(self, **kwargs): - super(TrilioVaultSubordinateCharm, self).__init__(**kwargs) + +class TrilioVaultSubordinateCharm(TrilioVaultCharmMixin, + charms_openstack.charm.OpenStackCharm, + metaclass=BaseTrilioCharmMeta): + + abstract_class = True def configure_source(self): """Configure TrilioVault specific package sources @@ -132,17 +541,6 @@ class TrilioVaultSubordinateCharm(charms_openstack.charm.OpenStackCharm): _configure_triliovault_source() fetch.apt_update(fatal=True) - def install(self): - """Install packages dealing with Trilio nuances for upgrades as well - """ - self.configure_source() - _install_triliovault(self) - - def series_upgrade_complete(self): - """Re-configure sources post series upgrade""" - super().series_upgrade_complete() - self.configure_source() - class TrilioVaultCharmGhostAction(object): """Shared 'ghost share' action for TrilioVault charms diff --git a/unit_tests/charms_openstack/charm/test_core.py b/unit_tests/charms_openstack/charm/test_core.py index 9dca545..46b33ef 100644 --- a/unit_tests/charms_openstack/charm/test_core.py +++ b/unit_tests/charms_openstack/charm/test_core.py @@ -375,12 +375,22 @@ class TestBaseOpenStackCharmAssessStatus(BaseOpenStackCharmTest): class TestMyOpenStackCharm(BaseOpenStackCharmTest): def setUp(self): + self.save_rsf = chm_core._release_selector_function + chm_core._release_selector_function = None + self.save_cif = chm_core._get_charm_instance_function + chm_core._get_charm_instance_function = None + def make_open_stack_charm(): return MyOpenStackCharm(['interface1', 'interface2']) super(TestMyOpenStackCharm, self).setUp(make_open_stack_charm, TEST_CONFIG) + def tearDown(self): + chm_core._release_selector_function = self.save_rsf + chm_core._get_charm_instance_function = self.save_cif + super().tearDown() + def test_singleton(self): # because we have two releases, we expect this to be the latter. # e.g. MyNextOpenStackCharm diff --git a/unit_tests/charms_openstack/plugins/test_trilio.py b/unit_tests/charms_openstack/plugins/test_trilio.py index 4b7f6b5..833c22c 100644 --- a/unit_tests/charms_openstack/plugins/test_trilio.py +++ b/unit_tests/charms_openstack/plugins/test_trilio.py @@ -2,8 +2,9 @@ import unittest.mock as mock import os from unit_tests.charms_openstack.charm.utils import BaseOpenStackCharmTest -from unit_tests.utils import patch_open +from unit_tests.utils import BaseTestCase, patch_open +import charms_openstack.charm.core as co_core import charms_openstack.plugins.trilio as trilio @@ -12,6 +13,11 @@ class TrilioVaultFoobar(trilio.TrilioVaultCharm): abstract_class = True name = 'test' all_packages = ['foo', 'bar'] + os_release_pkg = 'nova-common' + + @classmethod + def trilio_version_package(cls): + return "dmapi" class TrilioVaultFoobarSubordinate(trilio.TrilioVaultSubordinateCharm): @@ -114,6 +120,7 @@ class TestTrilioCommonBehaviours(BaseOpenStackCharmTest): self.patch_object(trilio.ch_core.hookenv, "status_set") self.patch_object(trilio.fetch, "filter_installed_packages") self.patch_object(trilio.fetch, "apt_install") + self.patch_object(trilio.fetch.apt_pkg, 'version_compare') self.patch_object(trilio.reactive, "is_flag_set") self.patch_object(trilio.reactive, "clear_flag") self.patch_target('update_api_ports') @@ -162,13 +169,216 @@ class TestTrilioCommonBehaviours(BaseOpenStackCharmTest): ) _file.write.assert_called_once_with('testsource') + def test_trilio_properties(self): + cls_mock = mock.MagicMock() + cls_mock.charm_instance.release_pkg_version = lambda: '4.0' + self.version_compare.return_value = 0 + self.assertEqual( + trilio.trilio_properties(cls_mock), + {'db_type': 'dedicated', 'transport_type': 'dmapi'}) + self.version_compare.return_value = -1 + self.assertEqual( + trilio.trilio_properties(cls_mock), + {'db_type': 'legacy', 'transport_type': 'legacy'}) + + def test_get_trilio_codename_install_source(self): + self.assertEqual( + trilio.get_trilio_codename_install_source( + 'deb [trusted=yes] https://apt.fury.io/triliodata-4-0/ /'), + '4.0') + self.assertEqual( + trilio.get_trilio_codename_install_source( + 'deb [trusted=yes] https://apt.fury.io/triliodata-4-0-0/ /'), + '4.0') + with self.assertRaises(AssertionError): + trilio.get_trilio_codename_install_source( + 'deb [trusted=yes] https://apt.fury.io/triliodata/ /') + + def test_get_trilio_charm_instance(self): + _safe_gcif = co_core._get_charm_instance_function + co_core._get_charm_instance_function = None + + class BaseClass(): + def __init__(self, release, *args, **kwargs): + pass + + class Pike39(BaseClass): + release = 'pike' + trilio_release = '3.9' + + class Queens40(BaseClass): + release = 'queens' + trilio_release = '4.0' + + class Queens41(BaseClass): + release = 'queens' + trilio_release = '4.1' + + class Rocky40(BaseClass): + release = 'rocky' + trilio_release = '4.0' + + def _version_compare(ver1, ver2): + if float(ver1) > float(ver2): + return 1 + elif float(ver1) < float(ver2): + return -1 + else: + return 0 + + save_releases = trilio._trilio_releases + self.version_compare.side_effect = _version_compare + trilio._trilio_releases = { + 'pike': { + trilio.AptPkgVersion('3.9'): { + 'deb': Pike39}}, + 'queens': { + trilio.AptPkgVersion('4.0'): { + 'deb': Queens40}, + trilio.AptPkgVersion('4.1'): { + 'deb': Queens41}}, + 'rocky': { + trilio.AptPkgVersion('4.0'): { + 'deb': Rocky40}}} + trilio.make_trilio_get_charm_instance_handler() + # Check with no release being supplied. Should return the + # highest release class. + self.assertIsInstance( + co_core.get_charm_instance(), + Rocky40) + self.assertIsInstance( + co_core.get_charm_instance(release='queens_4.0'), + Queens40) + self.assertIsInstance( + co_core.get_charm_instance(release='queens_4.1'), + Queens41) + # Ensure an error is raised if a class satisfying the trilio condition + # is not found for the highest matching OpenStack class. + with self.assertRaises(RuntimeError): + co_core.get_charm_instance(release='rocky_3.9') + # Match the openstack release and then the closest trilio releases + # within that subset. + self.assertIsInstance( + co_core.get_charm_instance(release='rocky_4.1'), + Rocky40) + with self.assertRaises(RuntimeError): + co_core.get_charm_instance(release='icehouse_4.1') + trilio._trilio_releases = save_releases + co_core._get_charm_instance_function = _safe_gcif + + def test_select_trilio_release(self): + def get_charm_class(release_pkg='trilio_pkg', package_version='4.0', + os_codename_exception=None, + version_package='trilio_pkg', + package_version_exception=None, + os_release_pkg='nova_pkg', + os_codename_pkg='queens', + trilio_source='deb https://a.io/trilio-4-2-0/ /'): + + class _TrilioCharm(): + + def __init__(self): + self.release_pkg = release_pkg + self.version_package = version_package + self.os_release_pkg = os_release_pkg + self.source_config_key = 'openstack-origin' + self.package_codenames = {} + self.package_version = package_version + self.os_codename_exception = os_codename_exception + self.os_codename_pkg = os_codename_pkg + self.trilio_source = trilio_source + + @staticmethod + def get_os_codename_package(pkg, code_names, + apt_cache_sufficient=True): + if os_codename_exception: + raise os_codename_exception + else: + return os_codename_pkg + + @staticmethod + def get_package_version(pkg, apt_cache_sufficient=True): + if package_version_exception: + raise package_version_exception + else: + return package_version + + return _TrilioCharm() + + _safe_rsf = co_core._release_selector_function + co_core._release_selector_function = None + self.patch_object( + trilio.os_utils, + "get_installed_semantic_versioned_packages") + self.patch_object(trilio.os_utils, "os_release") + self.patch_object(trilio.unitdata, "kv") + kv_mock = mock.MagicMock() + self.kv.return_value = kv_mock + kv_mock.get.return_value = None + self.patch_object( + trilio.charms_openstack.charm.core, + "get_charm_instance") + + trilio.make_trilio_get_charm_instance_handler() + trilio.make_trilio_select_release_handler() + select_trilio_release = co_core._release_selector_function + self.get_charm_instance.return_value = get_charm_class() + self.assertEqual( + select_trilio_release(), + 'queens_4.0') + + # Check RuntimeError is raised if release_pkg is missing from charm + # class + self.get_charm_instance.return_value = get_charm_class( + release_pkg=None) + with self.assertRaises(RuntimeError): + select_trilio_release() + + # Test falling back to get_installed_semantic_versioned_packages + self.os_release.return_value = 'pike' + self.get_installed_semantic_versioned_packages.reset_mock() + self.get_installed_semantic_versioned_packages.return_value = ['nova'] + self.get_charm_instance.return_value = get_charm_class( + os_codename_pkg=None) + self.assertEqual( + select_trilio_release(), + 'pike_4.0') + + # Check RuntimeError is raised if version_package is missing from charm + # class + self.get_charm_instance.return_value = get_charm_class( + version_package=None) + with self.assertRaises(RuntimeError): + select_trilio_release() + + # Test falling back to get_trilio_codename_install_source + self.get_charm_instance.return_value = get_charm_class( + package_version_exception=ValueError) + self.assertEqual( + select_trilio_release(), + 'queens_4.2') + co_core._release_selector_function = _safe_rsf + class TestTrilioVaultCharm(BaseOpenStackCharmTest): def setUp(self): super().setUp(TrilioVaultFoobar, {}) + self.patch_object(trilio.ch_core.hookenv, "log") + self.patch_object(trilio.ch_core.hookenv, "status_set") + self.patch_object( + trilio.charms_openstack.charm.core, + "get_charm_instance") self.patch_object(trilio, "_install_triliovault") self.patch_object(trilio, "_configure_triliovault_source") + self.patch_object(trilio.fetch, "apt_update") + self.patch_object(trilio.fetch, "apt_install") + self.patch_object(trilio.fetch.apt_pkg, "version_compare") + self.patch_target('config') + self._conf = { + 'triliovault-pkg-source': 'deb https://a.io/trilio-4-2-0/ /' + } + self.config.get.side_effect = lambda x, b=None: self._conf.get(x, b) def test_series_upgrade_complete(self): self.patch_object(trilio.charms_openstack.charm.HAOpenStackCharm, @@ -190,6 +400,66 @@ class TestTrilioVaultCharm(BaseOpenStackCharmTest): self.target.install() self._install_triliovault.assert_called_once_with(self.target) + def test_trilio_source(self): + self.assertEqual( + self.target.trilio_source, + 'deb https://a.io/trilio-4-2-0/ /') + + def test_do_trilio_pkg_upgrade(self): + self.target.do_trilio_pkg_upgrade() + self.apt_update.assert_called_once_with() + self.apt_install.assert_called_once_with( + packages=['foo', 'bar'], + options=[ + '--option', 'Dpkg::Options::=--force-confnew', + '--option', 'Dpkg::Options::=--force-confdef'], + fatal=True) + + def test_run_trilio_upgrade(self): + self.patch_target('get_os_codename_package') + self.get_os_codename_package.return_value = 'queens' + charm_cls = mock.MagicMock() + interface_mocks = [mock.MagicMock(), mock.MagicMock()] + self.get_charm_instance.return_value = charm_cls + self.target.run_trilio_upgrade(interfaces_list=interface_mocks) + self._configure_triliovault_source.assert_called_once_with() + charm_cls.do_trilio_pkg_upgrade.assert_called_once_with() + charm_cls.render_with_interfaces.assert_called_once_with( + interface_mocks) + charm_cls.do_trilio_upgrade_db_migration.assert_called_once_with() + + def test_trilio_upgrade_available(self): + self.patch_target('get_package_version') + self.get_package_version.return_value = '4.1' + self.version_compare.return_value = 1 + self.assertTrue(self.target.trilio_upgrade_available()) + self.version_compare.assert_called_once_with('4.2', '4.1') + + def test_upgrade_if_available(self): + self.patch_target('openstack_upgrade_available') + self.patch_target('trilio_upgrade_available') + self.patch_target('run_upgrade') + self.patch_target('run_trilio_upgrade') + interface_mocks = [mock.MagicMock(), mock.MagicMock()] + + self._conf['action-managed-upgrade'] = False + self.openstack_upgrade_available.return_value = True + self.trilio_upgrade_available.return_value = True + self.target.upgrade_if_available(interface_mocks) + self.run_upgrade.assert_called_once_with( + interfaces_list=interface_mocks) + self.run_trilio_upgrade.assert_called_once_with( + interfaces_list=interface_mocks) + + self.run_upgrade.reset_mock() + self.run_trilio_upgrade.reset_mock() + self._conf['action-managed-upgrade'] = True + self.openstack_upgrade_available.return_value = True + self.trilio_upgrade_available.return_value = True + self.target.upgrade_if_available(interface_mocks) + self.assertFalse(self.run_upgrade.called) + self.assertFalse(self.run_trilio_upgrade.called) + class TestTrilioVaultSubordinateCharm(BaseOpenStackCharmTest): @@ -197,13 +467,7 @@ class TestTrilioVaultSubordinateCharm(BaseOpenStackCharmTest): super().setUp(TrilioVaultFoobarSubordinate, {}) self.patch_object(trilio, "_install_triliovault") self.patch_object(trilio, "_configure_triliovault_source") - - def test_series_upgrade_complete(self): - self.patch_object(trilio.charms_openstack.charm.OpenStackCharm, - 'series_upgrade_complete') - self.patch_target('configure_source') - self.target.series_upgrade_complete() - self.configure_source.assert_called_once_with() + self.patch_object(trilio.fetch, "apt_update") def test_configure_source(self): self.patch_object(trilio.charms_openstack.charm.OpenStackCharm, @@ -211,9 +475,100 @@ class TestTrilioVaultSubordinateCharm(BaseOpenStackCharmTest): self.target.configure_source() self._configure_triliovault_source.assert_called_once_with() self.configure_source.assert_not_called() + self.apt_update.assert_called_once_with(fatal=True) - def test_install(self): - self.patch_object(trilio.charms_openstack.charm.OpenStackCharm, - 'configure_source') - self.target.install() - self._install_triliovault.assert_called_once_with(self.target) + +class TestBaseTrilioCharmMeta(BaseTestCase): + + def setUp(self): + self.save_releases = trilio._trilio_releases + super().setUp() + self.patch_object(trilio.fetch.apt_pkg, 'version_compare') + + def _version_compare(ver1, ver2): + if float(ver1) > float(ver2): + return 1 + elif float(ver1) < float(ver2): + return -1 + else: + return 0 + self.version_compare.side_effect = _version_compare + + def tearDown(self): + super().tearDown() + trilio._trilio_releases = self.save_releases + + def register_classes(self): + + class TrilioQueens40(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + trilio_release = '4.0' + + class TrilioQueens41(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + trilio_release = '4.1' + + class TrilioRocky40(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'rocky' + trilio_release = '4.0' + + return { + 'queens_4.0': TrilioQueens40, + 'queens_4.1': TrilioQueens41, + 'rocky_4.0': TrilioRocky40} + + def register_classes_missing_key(self): + + class TrilioQueens40(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + + def register_classes_wrong_pkg_type(self): + + class TrilioQueens40(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + trilio_release = '4.1' + package_type = 'up2date' + + def register_classes_duplicate(self): + + class TrilioQueens40A(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + trilio_release = '4.0' + + class TrilioQueens40B(metaclass=trilio.BaseTrilioCharmMeta): + + release = 'queens' + trilio_release = '4.0' + + def test_class_register(self): + charm_classes = self.register_classes() + self.maxDiff = None + self.assertEqual( + trilio._trilio_releases, + { + 'queens': { + trilio.AptPkgVersion('4.0'): { + 'deb': charm_classes['queens_4.0']}, + trilio.AptPkgVersion('4.1'): { + 'deb': charm_classes['queens_4.1']}}, + 'rocky': { + trilio.AptPkgVersion('4.0'): { + 'deb': charm_classes['rocky_4.0']}}}) + + def test_class_register_missing_key(self): + with self.assertRaises(RuntimeError): + self.register_classes_missing_key() + + def test_class_register_wrong_pkg_type(self): + with self.assertRaises(RuntimeError): + self.register_classes_wrong_pkg_type() + + def test_class_register_duplicate(self): + with self.assertRaises(RuntimeError): + self.register_classes_duplicate()