From 4c2486660fa3e201f3a39a0371d7bf310fe3ab3e Mon Sep 17 00:00:00 2001 From: James Page Date: Tue, 8 Sep 2020 11:49:49 +0100 Subject: [PATCH] trilio: Support multiple NFS share configurations TrilioVault supports use of multiple NFS shares using a comma separated list. Update the ghost-nfs-share action code to support this type of configuration. The assumption is that the order of shares provided to the action is the same as the order of shares in the nfs-shares configuration option in terms of replication between sites. Mismatched length lists will generate an exception which can be propagated back to the operator. Change-Id: I7d501a3e44d4e7c732e7625fa2a8cde9a34ff564 Closes-Bug: 1894817 --- charms_openstack/plugins/trilio.py | 38 ++++++++++-- .../charms_openstack/plugins/test_trilio.py | 61 ++++++++++++++----- 2 files changed, 78 insertions(+), 21 deletions(-) diff --git a/charms_openstack/plugins/trilio.py b/charms_openstack/plugins/trilio.py index b4a8103..285ca14 100644 --- a/charms_openstack/plugins/trilio.py +++ b/charms_openstack/plugins/trilio.py @@ -44,6 +44,12 @@ class GhostShareAlreadyMountedException(Exception): pass +class MismatchedConfigurationException(Exception): + """Signal that nfs-shares and ghost-shares are mismatched""" + + pass + + def _configure_triliovault_source(): """Configure triliovault specific package sources in addition to any general openstack package sources (via openstack-origin) @@ -155,15 +161,37 @@ class TrilioVaultCharmGhostAction(object): """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 + def ghost_nfs_share(self, ghost_shares): + """Bind mount local NFS shares to remote NFS paths + :param ghost_shares: Comma separated NFS shares URL to ghost + :type ghost_shares: str + """ + ghost_shares = ghost_shares.split(',') + nfs_shares = ch_core.hookenv.config("nfs-shares").split(',') + try: + share_mappings = [ + (nfs_shares[i], ghost_shares[i]) + for i in range(0, len(nfs_shares)) + ] + except IndexError: + raise MismatchedConfigurationException( + "ghost-shares and nfs-shares are different lengths" + ) + for local_share, ghost_share in share_mappings: + self._ghost_nfs_share(local_share, ghost_share) + + def _ghost_nfs_share(self, local_share, ghost_share): + """Bind mount a local unit NFS share to another sites location + + :param local_share: Local NFS share URL + :type local_share: str :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")) + self._encode_endpoint(local_share) ) ghost_share_path = os.path.join( TV_MOUNTS, self._encode_endpoint(ghost_share) @@ -174,8 +202,8 @@ class TrilioVaultCharmGhostAction(object): 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") + "nfs-share ({}) not mounted".format( + local_share ) ) diff --git a/unit_tests/charms_openstack/plugins/test_trilio.py b/unit_tests/charms_openstack/plugins/test_trilio.py index 10080a1..4b7f6b5 100644 --- a/unit_tests/charms_openstack/plugins/test_trilio.py +++ b/unit_tests/charms_openstack/plugins/test_trilio.py @@ -1,3 +1,4 @@ +import unittest.mock as mock import os from unit_tests.charms_openstack.charm.utils import BaseOpenStackCharmTest @@ -22,8 +23,8 @@ class TrilioVaultFoobarSubordinate(trilio.TrilioVaultSubordinateCharm): class TestTrilioCharmGhostAction(BaseOpenStackCharmTest): - _nfs_shares = "10.20.30.40:/srv/trilioshare" - _ghost_shares = "50.20.30.40:/srv/trilioshare" + _nfs_share = "10.20.30.40:/srv/trilioshare" + _ghost_share = "50.20.30.40:/srv/trilioshare" def setUp(self): super().setUp(trilio.TrilioVaultCharmGhostAction, {}) @@ -36,46 +37,74 @@ class TestTrilioCharmGhostAction(BaseOpenStackCharmTest): self.trilio_charm = trilio.TrilioVaultCharmGhostAction() self._nfs_path = os.path.join( trilio.TV_MOUNTS, - self.trilio_charm._encode_endpoint(self._nfs_shares), + self.trilio_charm._encode_endpoint(self._nfs_share), ) self._ghost_path = os.path.join( trilio.TV_MOUNTS, - self.trilio_charm._encode_endpoint(self._ghost_shares), + self.trilio_charm._encode_endpoint(self._ghost_share), ) - def test_ghost_share(self): - self.config.return_value = self._nfs_shares + def test__ghost_nfs_share(self): + self.config.return_value = self._nfs_share self.mounts.return_value = [ ["/srv/nova", "/dev/sda"], - [self._nfs_path, self._nfs_shares], + [self._nfs_path, self._nfs_share], ] self.exists.return_value = False - self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.trilio_charm._ghost_nfs_share(self._nfs_share, + self._ghost_share) 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 + def test__ghost_nfs_share_already_bound(self): + self.config.return_value = self._nfs_share self.mounts.return_value = [ ["/srv/nova", "/dev/sda"], - [self._nfs_path, self._nfs_shares], - [self._ghost_path, self._nfs_shares], + [self._nfs_path, self._nfs_share], + [self._ghost_path, self._nfs_share], ] with self.assertRaises(trilio.GhostShareAlreadyMountedException): - self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.trilio_charm._ghost_nfs_share(self._nfs_share, + self._ghost_share) self.mount.assert_not_called() - def test_ghost_share_nfs_unmounted(self): - self.config.return_value = self._nfs_shares + def test__ghost_nfs_share_nfs_unmounted(self): + self.config.return_value = self._nfs_share self.mounts.return_value = [["/srv/nova", "/dev/sda"]] self.exists.return_value = False with self.assertRaises(trilio.NFSShareNotMountedException): - self.trilio_charm.ghost_nfs_share(self._ghost_shares) + self.trilio_charm._ghost_nfs_share(self._nfs_share, + self._ghost_share) self.mount.assert_not_called() + def test_ghost_nfs_share(self): + self.patch_object(self.trilio_charm, "_ghost_nfs_share") + self.config.return_value = ( + "10.20.30.40:/srv/trilioshare,10.20.30.40:/srv/trilioshare2" + ) + self.trilio_charm.ghost_nfs_share( + "50.20.30.40:/srv/trilioshare,50.20.30.40:/srv/trilioshare2" + ) + self._ghost_nfs_share.assert_has_calls([ + mock.call("10.20.30.40:/srv/trilioshare", + "50.20.30.40:/srv/trilioshare"), + mock.call("10.20.30.40:/srv/trilioshare2", + "50.20.30.40:/srv/trilioshare2") + ]) + + def test_ghost_nfs_share_mismatch(self): + self.patch_object(self.trilio_charm, "_ghost_nfs_share") + self.config.return_value = ( + "10.20.30.40:/srv/trilioshare,10.20.30.40:/srv/trilioshare2" + ) + with self.assertRaises(trilio.MismatchedConfigurationException): + self.trilio_charm.ghost_nfs_share( + "50.20.30.40:/srv/trilioshare" + ) + class TestTrilioCommonBehaviours(BaseOpenStackCharmTest):