diff --git a/charms_openstack/plugins/__init__.py b/charms_openstack/plugins/__init__.py index 0b2a1c5..12b193d 100644 --- a/charms_openstack/plugins/__init__.py +++ b/charms_openstack/plugins/__init__.py @@ -25,6 +25,7 @@ from charms_openstack.plugins.trilio import ( TrilioVaultCharm, TrilioVaultSubordinateCharm, TrilioVaultCharmGhostAction, + TrilioVault42CharmGhostAction, ) __all__ = ( @@ -35,4 +36,5 @@ __all__ = ( "TrilioVaultCharm", "TrilioVaultSubordinateCharm", "TrilioVaultCharmGhostAction", + "TrilioVault42CharmGhostAction", ) diff --git a/charms_openstack/plugins/trilio.py b/charms_openstack/plugins/trilio.py index 59ac4c1..dffa61b 100644 --- a/charms_openstack/plugins/trilio.py +++ b/charms_openstack/plugins/trilio.py @@ -598,6 +598,20 @@ class TrilioVaultCharmGhostAction(object): for local_share, ghost_share in share_mappings: self._ghost_nfs_share(local_share, ghost_share) + def trilio_share_mounted(self, share_path): + """Check if share_path is mounted + + :param local_share: Local NFS share URL + :type local_share: str + :returns: Whether share is mounted + :rtype: bool + """ + _share_path = os.path.join( + TV_MOUNTS, + self._encode_endpoint(share_path)) + current_mounts = [mount[0] for mount in ch_core.host.mounts()] + return _share_path in current_mounts + def _ghost_nfs_share(self, local_share, ghost_share): """Bind mount a local unit NFS share to another sites location @@ -614,9 +628,7 @@ class TrilioVaultCharmGhostAction(object): 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: + if not self.trilio_share_mounted(local_share): # Trilio has not mounted the NFS share so return raise NFSShareNotMountedException( "nfs-share ({}) not mounted".format( @@ -624,7 +636,7 @@ class TrilioVaultCharmGhostAction(object): ) ) - if ghost_share_path in current_mounts: + if self.trilio_share_mounted(ghost_share): # bind mount already setup so return raise GhostShareAlreadyMountedException( "ghost mountpoint ({}) already bound".format(ghost_share_path) @@ -634,3 +646,39 @@ class TrilioVaultCharmGhostAction(object): os.mkdir(ghost_share_path) ch_core.host.mount(nfs_share_path, ghost_share_path, options="bind") + + +class TrilioVault42CharmGhostAction(TrilioVaultCharmGhostAction): + + def _encode_endpoint_uri(self, backup_endpoint): + """base64 encode a backup uri for cross mounting support""" + return base64.b64encode(backup_endpoint.encode()).decode() + + def _encode_endpoint_path(self, backup_endpoint): + """base64 encode an backup path for cross mounting support""" + return base64.b64encode( + str.encode(urlparse(backup_endpoint).path)).decode() + + def _encode_endpoint(self, backup_endpoint): + """base64 encode an backup endpoint for cross mounting support""" + return self._encode_endpoint_path(backup_endpoint) + + def trilio_share_mounted(self, share_path): + """Check if share_path is mounted + + :param local_share: Local NFS share URL + :type local_share: str + :returns: Whether share is mounted + :rtype: bool + """ + mount_paths = [ + os.path.join( + TV_MOUNTS, + self._encode_endpoint_path(share_path) + ), + os.path.join( + TV_MOUNTS, + self._encode_endpoint_uri(share_path) + )] + current_mounts = [mount[0] for mount in ch_core.host.mounts()] + return any(m in current_mounts for m in mount_paths) diff --git a/unit_tests/charms_openstack/plugins/test_trilio.py b/unit_tests/charms_openstack/plugins/test_trilio.py index 17ad1eb..142df95 100644 --- a/unit_tests/charms_openstack/plugins/test_trilio.py +++ b/unit_tests/charms_openstack/plugins/test_trilio.py @@ -30,7 +30,7 @@ class TrilioVaultFoobarSubordinate(trilio.TrilioVaultSubordinateCharm): class TestTrilioCharmGhostAction(BaseOpenStackCharmTest): _nfs_share = "10.20.30.40:/srv/trilioshare" - _ghost_share = "50.20.30.40:/srv/trilioshare" + _ghost_share = "50.20.30.40:/srv/trilioghostshare" def setUp(self): super().setUp(trilio.TrilioVaultCharmGhostAction, {}) @@ -112,6 +112,91 @@ class TestTrilioCharmGhostAction(BaseOpenStackCharmTest): ) +class TestTrilioVault42CharmGhostAction(BaseOpenStackCharmTest): + + _nfs_share = "10.20.30.40:/srv/trilioshare" + _ghost_share = "50.20.30.40:/srv/trilioghostshare" + + def setUp(self): + super().setUp(trilio.TrilioVaultCharmGhostAction, {}) + self.patch_object(trilio.ch_core.hookenv, "config") + self.patch_object(trilio.ch_core.host, "mounts") + self.patch_object(trilio.ch_core.host, "mount") + self.patch_object(trilio.os.path, "exists") + self.patch_object(trilio.os, "mkdir") + + self.trilio_charm = trilio.TrilioVault42CharmGhostAction() + self._nfs_path = os.path.join( + trilio.TV_MOUNTS, + self.trilio_charm._encode_endpoint(self._nfs_share), + ) + self._ghost_path = os.path.join( + trilio.TV_MOUNTS, + self.trilio_charm._encode_endpoint(self._ghost_share), + ) + + 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_share], + ] + self.exists.return_value = False + 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_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_share], + [self._ghost_path, self._nfs_share], + ] + with self.assertRaises(trilio.GhostShareAlreadyMountedException): + self.trilio_charm._ghost_nfs_share(self._nfs_share, + self._ghost_share) + self.mount.assert_not_called() + + 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._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): def setUp(self):