Add TrilioVault charm classes

Add two helper classes to support common behaviour across the
TrilioVault charm set.

Change-Id: Ibe6c920f577326dfe0f6c3339de69a3cc060eb36
This commit is contained in:
James Page 2020-06-05 12:00:21 +01:00
parent f0a486c59d
commit 037ea8bdf7
2 changed files with 271 additions and 0 deletions

View File

@ -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)

View File

@ -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()