From 037ea8bdf7262f736e1aae998316c53000ff3f7f Mon Sep 17 00:00:00 2001 From: James Page Date: Fri, 5 Jun 2020 12:00:21 +0100 Subject: [PATCH] Add TrilioVault charm classes Add two helper classes to support common behaviour across the TrilioVault charm set. Change-Id: Ibe6c920f577326dfe0f6c3339de69a3cc060eb36 --- charms_openstack/plugins/classes.py | 135 +++++++++++++++++ .../charms_openstack/plugins/test_classes.py | 136 ++++++++++++++++++ 2 files changed, 271 insertions(+) diff --git a/charms_openstack/plugins/classes.py b/charms_openstack/plugins/classes.py index a24abe3..98fc820 100644 --- a/charms_openstack/plugins/classes.py +++ b/charms_openstack/plugins/classes.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import collections import os import shutil @@ -22,8 +23,11 @@ import charms_openstack.charm from charms_openstack.charm.classes import SNAP_PATH_PREFIX_FORMAT import charmhelpers.core as ch_core +import charmhelpers.fetch as fetch import charmhelpers.contrib.openstack.policyd as ch_policyd +import charms.reactive as reactive + class BaseOpenStackCephCharm(object): """Base class for Ceph classes. @@ -408,3 +412,134 @@ class PolicydOverridePlugin(object): args, kwargs = self._policyd_function_args() ch_policyd.maybe_do_policyd_overrides_on_config_changed( *args, **kwargs) + + +TV_MOUNTS = "/var/triliovault-mounts" + + +class NFSShareNotMountedException(Exception): + """Signal that the trilio nfs share is not mount""" + + pass + + +class UnitNotLeaderException(Exception): + """Signal that the unit is not the application leader""" + + pass + + +class GhostShareAlreadyMountedException(Exception): + """Signal that a ghost share is already mounted""" + + pass + + +class TrilioVaultCharm(charms_openstack.charm.HAOpenStackCharm): + """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 + """ + + abstract_class = True + + def __init__(self, **kwargs): + super(TrilioVaultCharm, self).__init__(**kwargs) + + def configure_source(self): + """Configure triliovault specific package sources in addition to + any general openstack package sources (via openstack-origin) + """ + with open( + "/etc/apt/sources.list.d/trilio-gemfury-sources.list", "w" + ) as tsources: + tsources.write(ch_core.hookenv.config("triliovault-pkg-source")) + super().configure_source() + + def install(self): + """Install packages dealing with Trilio nuances for upgrades as well + + Set the 'upgrade.triliovault' flag to ensure that any triliovault + packages are upgraded. + """ + self.configure_source() + packages = self.all_packages + if not reactive.is_flag_set("upgrade.triliovault"): + packages = fetch.filter_installed_packages( + self.all_packages) + + if packages: + ch_core.hookenv.status_set('maintenance', + 'Installing/upgrading packages') + fetch.apt_install(packages, fatal=True) + + # AJK: we set this as charms can use it to detect installed state + self.set_state('{}-installed'.format(self.name)) + self.update_api_ports() + + # NOTE(jamespage): clear upgrade flag if set + if reactive.is_flag_set("upgrade.triliovault"): + reactive.clear_flag('upgrade.triliovault') + + def series_upgrade_complete(self): + """Re-configure sources post series upgrade""" + super().series_upgrade_complete() + self.configure_source() + + def _encode_endpoint(self, backup_endpoint): + """base64 encode an backup endpoint for cross mounting support""" + return base64.b64encode(backup_endpoint.encode()).decode() + + def ghost_nfs_share(self, ghost_share): + """Bind mount the local units nfs share to another sites location + + :param ghost_share: NFS share URL to ghost + :type ghost_share: str + """ + nfs_share_path = os.path.join( + TV_MOUNTS, + self._encode_endpoint(ch_core.hookenv.config("nfs-shares")) + ) + ghost_share_path = os.path.join( + TV_MOUNTS, self._encode_endpoint(ghost_share) + ) + + current_mounts = [mount[0] for mount in ch_core.host.mounts()] + + if nfs_share_path not in current_mounts: + # Trilio has not mounted the NFS share so return + raise NFSShareNotMountedException( + "nfs-shares ({}) not mounted".format( + ch_core.hookenv.config("nfs-shares") + ) + ) + + if ghost_share_path in current_mounts: + # bind mount already setup so return + raise GhostShareAlreadyMountedException( + "ghost mountpoint ({}) already bound".format(ghost_share_path) + ) + + if not os.path.exists(ghost_share_path): + os.mkdir(ghost_share_path) + + ch_core.host.mount(nfs_share_path, ghost_share_path, options="bind") + + +class TrilioVaultSubordinateCharm(TrilioVaultCharm): + """The TrilioVaultSubordinateCharm class provides common specialisation + of certain functions for the Trilio charm set and is designed for usei + alongside other base charms.openstack classes for subordinate charms + """ + + abstract_class = True + + def __init__(self, **kwargs): + super(TrilioVaultSubordinateCharm, self).__init__(**kwargs) + + def configure_source(self): + """Configure triliovault specific package sources in addition to + any general openstack package sources (via openstack-origin) + """ + super().configure_source() + fetch.apt_update(fatal=True) diff --git a/unit_tests/charms_openstack/plugins/test_classes.py b/unit_tests/charms_openstack/plugins/test_classes.py index 196f487..5c2c0e5 100644 --- a/unit_tests/charms_openstack/plugins/test_classes.py +++ b/unit_tests/charms_openstack/plugins/test_classes.py @@ -3,10 +3,12 @@ import os import subprocess from unit_tests.charms_openstack.charm.utils import BaseOpenStackCharmTest +from unit_tests.utils import patch_open import charms_openstack.charm.classes as chm import charms_openstack.plugins.classes as cpl + TEST_CONFIG = {'config': True, 'openstack-origin': None} @@ -289,3 +291,137 @@ class TestPolicydOverridePlugin(BaseOpenStackCharmTest): self._policyd_function_args.assert_called_once_with() self.mock_policyd_call.assert_called_once_with( "args", kwargs=1) + + +class TestTrilioCharmGhostShareAction(BaseOpenStackCharmTest): + + _nfs_shares = "10.20.30.40:/srv/trilioshare" + _ghost_shares = "50.20.30.40:/srv/trilioshare" + + def setUp(self): + super().setUp(cpl.TrilioVaultCharm, {}) + self.patch_object(cpl.ch_core.hookenv, "config") + self.patch_object(cpl.ch_core.host, "mounts") + self.patch_object(cpl.ch_core.host, "mount") + self.patch_object(cpl.os.path, "exists") + self.patch_object(cpl.os, "mkdir") + + self.trilio_charm = cpl.TrilioVaultCharm() + self._nfs_path = os.path.join( + cpl.TV_MOUNTS, + self.trilio_charm._encode_endpoint(self._nfs_shares), + ) + self._ghost_path = os.path.join( + cpl.TV_MOUNTS, + self.trilio_charm._encode_endpoint(self._ghost_shares), + ) + + def test_ghost_share(self): + self.config.return_value = self._nfs_shares + self.mounts.return_value = [ + ["/srv/nova", "/dev/sda"], + [self._nfs_path, self._nfs_shares], + ] + self.exists.return_value = False + self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.exists.assert_called_once_with(self._ghost_path) + self.mkdir.assert_called_once_with(self._ghost_path) + self.mount.assert_called_once_with( + self._nfs_path, self._ghost_path, options="bind" + ) + + def test_ghost_share_already_bound(self): + self.config.return_value = self._nfs_shares + self.mounts.return_value = [ + ["/srv/nova", "/dev/sda"], + [self._nfs_path, self._nfs_shares], + [self._ghost_path, self._nfs_shares], + ] + with self.assertRaises(cpl.GhostShareAlreadyMountedException): + self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.mount.assert_not_called() + + def test_ghost_share_nfs_unmounted(self): + self.config.return_value = self._nfs_shares + self.mounts.return_value = [["/srv/nova", "/dev/sda"]] + self.exists.return_value = False + with self.assertRaises(cpl.NFSShareNotMountedException): + self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.mount.assert_not_called() + + +class TrilioVaultFoobar(cpl.TrilioVaultCharm): + + abstract_class = True + name = 'test' + all_packages = ['foo', 'bar'] + + +class TestTrilioCharmBehaviours(BaseOpenStackCharmTest): + + def setUp(self): + super().setUp(TrilioVaultFoobar, {}) + self.patch_object(cpl.ch_core.hookenv, "config") + self.patch_object(cpl.ch_core.hookenv, "status_set") + self.patch_object(cpl.fetch, "filter_installed_packages") + self.patch_object(cpl.fetch, "apt_install") + self.patch_object(cpl.reactive, "is_flag_set") + self.patch_object(cpl.reactive, "clear_flag") + self.patch_target('update_api_ports') + self.patch_target('set_state') + self.filter_installed_packages.side_effect = lambda p: p + + def test_install(self): + self.patch_target('configure_source') + self.is_flag_set.return_value = False + + self.target.install() + + self.is_flag_set.assert_called_with('upgrade.triliovault') + self.filter_installed_packages.assert_called_once_with( + self.target.all_packages + ) + self.apt_install.assert_called_once_with( + self.target.all_packages, + fatal=True + ) + self.clear_flag.assert_not_called() + self.set_state.assert_called_once_with('test-installed') + self.update_api_ports.assert_called_once() + self.configure_source.assert_called_once_with() + + def test_upgrade(self): + self.patch_target('configure_source') + self.is_flag_set.return_value = True + + self.target.install() + + self.is_flag_set.assert_called_with('upgrade.triliovault') + self.filter_installed_packages.assert_not_called() + self.apt_install.assert_called_once_with( + self.target.all_packages, + fatal=True + ) + self.clear_flag.assert_called_once_with('upgrade.triliovault') + self.set_state.assert_called_once_with('test-installed') + self.update_api_ports.assert_called_once() + self.configure_source.assert_called_once_with() + + def test_configure_source(self): + self.config.return_value = 'testsource' + self.patch_object(cpl.charms_openstack.charm.HAOpenStackCharm, + 'configure_source') + with patch_open() as (_open, _file): + self.target.configure_source() + _open.assert_called_with( + "/etc/apt/sources.list.d/trilio-gemfury-sources.list", + "w" + ) + _file.write.assert_called_once_with('testsource') + + def test_series_upgrade_complete(self): + self.patch_object(cpl.charms_openstack.charm.HAOpenStackCharm, + 'series_upgrade_complete') + self.patch_target('configure_source') + self.target.series_upgrade_complete() + self.configure_source.assert_called_once_with()