diff --git a/manila/share/drivers/glusterfs_native.py b/manila/share/drivers/glusterfs_native.py index dfe6715b6a..d9bb5393a0 100644 --- a/manila/share/drivers/glusterfs_native.py +++ b/manila/share/drivers/glusterfs_native.py @@ -621,6 +621,102 @@ class GlusterfsNativeShareDriver(driver.ExecuteMixin, driver.ShareDriver): # TODO(deepakcs): Disable quota. + @staticmethod + def _find_actual_backend_snapshot_name(gluster_mgr, snapshot): + args = ('snapshot', 'list', gluster_mgr.volume, '--mode=script') + try: + out, err = gluster_mgr.gluster_call(*args) + except exception.ProcessExecutionError as exc: + LOG.error(_LE("Error retrieving snapshot list: %s"), exc.stderr) + raise exception.GlusterfsException(_("gluster %s failed") % + ' '.join(args)) + snapgrep = list(filter(lambda x: snapshot['id'] in x, out.split("\n"))) + if len(snapgrep) != 1: + msg = (_("Failed to identify backing GlusterFS object " + "for snapshot %(snap_id)s of share %(share_id)s: " + "a single candidate was expected, %(found)d was found.") % + {'snap_id': snapshot['id'], + 'share_id': snapshot['share_id'], + 'found': len(snapgrep)}) + raise exception.GlusterfsException(msg) + backend_snapshot_name = snapgrep[0] + return backend_snapshot_name + + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None): + old_exportlocation = snapshot['share']['export_location'] + old_gmgr = self.gluster_used_vols_dict[old_exportlocation] + + # Snapshot clone feature in GlusterFS server essential to support this + # API is available in GlusterFS server versions 3.7 and higher. So do + # a version check. + vers = self.glusterfs_versions[old_gmgr.management_address] + minvers = (3, 7) + if glusterfs.GlusterManager.numreduct(vers) < minvers: + minvers_str = '.'.join(six.text_type(c) for c in minvers) + vers_str = '.'.join(vers) + msg = (_("GlusterFS version %(version)s on server %(server)s does " + "not support creation of shares from snapshot. " + "minimum requirement: %(minversion)s") % + {'version': vers_str, 'server': old_gmgr.host, + 'minversion': minvers_str}) + LOG.error(msg) + raise exception.GlusterfsException(msg) + + # Clone the snapshot. The snapshot clone, a new GlusterFS volume + # would serve as a share. + backend_snapshot_name = self._find_actual_backend_snapshot_name( + old_gmgr, snapshot) + volume = ''.join(['manila-', share['id']]) + args_tuple = (('snapshot', 'activate', backend_snapshot_name, + 'force', '--mode=script'), + ('snapshot', 'clone', volume, backend_snapshot_name)) + try: + for args in args_tuple: + out, err = old_gmgr.gluster_call(*args) + except exception.ProcessExecutionError as exc: + LOG.error(_LE("Error creating share from snapshot: %s"), + exc.stderr) + raise exception.GlusterfsException(_("gluster %s failed") % + ' '.join(args)) + + # Construct the volume address/export location of the new + # volume/share. + export_location = ':/'.join([old_gmgr.management_address, volume]) + + # Configure the GlusterFS volume to be used as share. + # 1. The clone of the snapshot, the new volume, retains the authorized + # access list of the snapshotted volume/share, which includes + # identities of the backend servers and Manila clients. So only + # retain the identities of the GlusterFS servers volume in the + # authorized access list of the new volume. The identities of + # GlusterFS are easy to figure as they're pre-fixed by + # "glusterfs-server". + # 2. Start the new volume. + gmgr = self._glustermanager(export_location) + old_access = gmgr.get_gluster_vol_option(AUTH_SSL_ALLOW) + # wrt. GlusterFS' parsing of auth.ssl-allow, please see code from + # https://github.com/gluster/glusterfs/blob/v3.6.2/ + # xlators/protocol/auth/login/src/login.c#L80 + # until end of gf_auth() function + old_access_list = re.split('[ ,]', old_access) + regex = re.compile('\Aglusterfs-server*') + access_to = ','.join(filter(regex.match, old_access_list)) + args_tuple = (('volume', 'set', gmgr.volume, AUTH_SSL_ALLOW, + access_to), + ('volume', 'start', gmgr.volume)) + try: + for args in args_tuple: + out, err = gmgr.gluster_call(*args) + except exception.ProcessExecutionError as exc: + LOG.error(_LE("Error creating share from snapshot: %s"), + exc.stderr) + raise exception.GlusterfsException(_("gluster %s failed") % + ' '.join(args)) + + self.gluster_used_vols_dict[export_location] = gmgr + return export_location + def create_snapshot(self, context, snapshot, share_server=None): """Creates a snapshot.""" @@ -673,23 +769,10 @@ class GlusterfsNativeShareDriver(driver.ExecuteMixin, driver.ShareDriver): vol = snapshot['share']['export_location'] gluster_mgr = self.gluster_used_vols_dict[vol] - args = ('snapshot', 'list', gluster_mgr.volume, '--mode=script') - try: - out, err = gluster_mgr.gluster_call(*args) - except exception.ProcessExecutionError as exc: - LOG.error(_LE("Error retrieving snapshot list: %s"), exc.stderr) - raise exception.GlusterfsException(_("gluster %s failed") % - ' '.join(args)) - snapgrep = list(filter(lambda x: snapshot['id'] in x, out.split("\n"))) - if len(snapgrep) != 1: - msg = (_("Failed to identify backing GlusterFS object " - "for snapshot %(snap_id)s of share %(share_id)s: " - "a single candidate was expected, %(found)d was found.") % - {'snap_id': snapshot['id'], - 'share_id': snapshot['share_id'], - 'found': len(snapgrep)}) - raise exception.GlusterfsException(msg) - args = ('--xml', 'snapshot', 'delete', snapgrep[0], '--mode=script') + backend_snapshot_name = self._find_actual_backend_snapshot_name( + gluster_mgr, snapshot) + args = ('--xml', 'snapshot', 'delete', backend_snapshot_name, + '--mode=script') try: out, err = gluster_mgr.gluster_call(*args) except exception.ProcessExecutionError as exc: diff --git a/manila/tests/share/drivers/test_glusterfs_native.py b/manila/tests/share/drivers/test_glusterfs_native.py index 30cbd50427..3c679158e6 100644 --- a/manila/tests/share/drivers/test_glusterfs_native.py +++ b/manila/tests/share/drivers/test_glusterfs_native.py @@ -869,6 +869,236 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): self.assertRaises(exctype, self._driver.create_snapshot, self._context, snapshot) + def test_find_actual_backend_snapshot_name(self): + gmgr = glusterfs.GlusterManager + gmgr1 = gmgr(self.share1['export_location'], self._execute, None, None) + self.mock_object(gmgr1, 'gluster_call', + mock.Mock(return_value=('fake_snap_id_xyz', ''))) + args = ('snapshot', 'list', gmgr1.volume, '--mode=script') + snapshot = { + 'id': 'fake_snap_id', + 'share_id': self.share1['id'], + 'share': self.share1 + } + ret = self._driver._find_actual_backend_snapshot_name(gmgr1, snapshot) + gmgr1.gluster_call.assert_called_once_with(*args) + self.assertEqual('fake_snap_id_xyz', ret) + + @ddt.data('this is too bad', 'fake_snap_id_xyx\nfake_snap_id_pqr') + def test_find_actual_backend_snapshot_name_bad_snap_list(self, snaplist): + gmgr = glusterfs.GlusterManager + gmgr1 = gmgr(self.share1['export_location'], self._execute, None, None) + self.mock_object(gmgr1, 'gluster_call', + mock.Mock(return_value=(snaplist, ''))) + args = ('snapshot', 'list', gmgr1.volume, '--mode=script') + snapshot = { + 'id': 'fake_snap_id', + 'share_id': self.share1['id'], + 'share': self.share1 + } + self.assertRaises(exception.GlusterfsException, + self._driver._find_actual_backend_snapshot_name, + gmgr1, snapshot) + gmgr1.gluster_call.assert_called_once_with(*args) + + @ddt.data({'glusterfs_target': 'root@host1:/gv1', + 'glusterfs_server': 'root@host1'}, + {'glusterfs_target': 'host1:/gv1', + 'glusterfs_server': 'host1'}) + @ddt.unpack + def test_create_share_from_snapshot(self, glusterfs_target, + glusterfs_server): + share = new_share() + snapshot = { + 'id': 'fake_snap_id', + 'share': new_share(export_location=glusterfs_target) + } + volume = ''.join(['manila-', share['id']]) + new_export_location = ':/'.join([glusterfs_server, volume]) + gmgr = glusterfs.GlusterManager + old_gmgr = gmgr(glusterfs_target, self._execute, None, None) + new_gmgr = gmgr(new_export_location, self._execute, None, None) + self._driver.gluster_used_vols_dict = {glusterfs_target: old_gmgr} + self._driver.glusterfs_versions = {glusterfs_server: ('3', '7')} + + self.mock_object(old_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), ('', '')])) + self.mock_object(new_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), ('', '')])) + self.mock_object(new_gmgr, 'get_gluster_vol_option', + mock.Mock()) + new_gmgr.get_gluster_vol_option.return_value = ( + 'glusterfs-server-1,client') + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + self.mock_object(self._driver, '_glustermanager', + mock.Mock(return_value=new_gmgr)) + + ret = self._driver.create_share_from_snapshot( + self._context, share, snapshot, None) + + (self._driver._find_actual_backend_snapshot_name. + assert_called_once_with(old_gmgr, snapshot)) + args = (('snapshot', 'activate', 'fake_snap_id_xyz', + 'force', '--mode=script'), + ('snapshot', 'clone', volume, 'fake_snap_id_xyz')) + old_gmgr.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + self._driver._glustermanager.assert_called_once_with( + new_export_location) + new_gmgr.get_gluster_vol_option.assert_called_once_with( + 'auth.ssl-allow') + args = (('volume', 'set', new_gmgr.volume, 'auth.ssl-allow', + 'glusterfs-server-1'), + ('volume', 'start', new_gmgr.volume), ) + new_gmgr.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + self.assertEqual( + new_gmgr, + self._driver.gluster_used_vols_dict[new_export_location]) + self.assertEqual(new_export_location, ret) + + def test_create_share_from_snapshot_error_old_gmr_gluster_calls(self): + glusterfs_target = 'root@host1:/gv1' + glusterfs_server = 'root@host1' + share = new_share() + snapshot = { + 'id': 'fake_snap_id', + 'share': new_share(export_location=glusterfs_target) + } + volume = ''.join(['manila-', share['id']]) + new_export_location = ':/'.join([glusterfs_server, volume]) + gmgr = glusterfs.GlusterManager + old_gmgr = gmgr(glusterfs_target, self._execute, None, None) + new_gmgr = gmgr(new_export_location, self._execute, None, None) + self._driver.gluster_used_vols_dict = {glusterfs_target: old_gmgr} + self._driver.glusterfs_versions = {glusterfs_server: ('3', '7')} + + self.mock_object( + old_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), exception.ProcessExecutionError])) + self.mock_object(new_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), ('', '')])) + self.mock_object(new_gmgr, 'get_gluster_vol_option', + mock.Mock()) + new_gmgr.get_gluster_vol_option.return_value = ( + 'glusterfs-server-1,client') + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + self.mock_object(self._driver, '_glustermanager', + mock.Mock(return_value=new_gmgr)) + + self.assertRaises(exception.GlusterfsException, + self._driver.create_share_from_snapshot, + self._context, share, snapshot) + + (self._driver._find_actual_backend_snapshot_name. + assert_called_once_with(old_gmgr, snapshot)) + args = (('snapshot', 'activate', 'fake_snap_id_xyz', + 'force', '--mode=script'), + ('snapshot', 'clone', volume, 'fake_snap_id_xyz')) + old_gmgr.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + self.assertFalse(new_gmgr.get_gluster_vol_option.called) + self.assertFalse(new_gmgr.gluster_call.called) + self.assertNotIn(new_export_location, + self._driver.glusterfs_versions.keys()) + + def test_create_share_from_snapshot_error_new_gmr_gluster_calls(self): + glusterfs_target = 'root@host1:/gv1' + glusterfs_server = 'root@host1' + share = new_share() + snapshot = { + 'id': 'fake_snap_id', + 'share': new_share(export_location=glusterfs_target) + } + volume = ''.join(['manila-', share['id']]) + new_export_location = ':/'.join([glusterfs_server, volume]) + gmgr = glusterfs.GlusterManager + old_gmgr = gmgr(glusterfs_target, self._execute, None, None) + new_gmgr = gmgr(new_export_location, self._execute, None, None) + self._driver.gluster_used_vols_dict = {glusterfs_target: old_gmgr} + self._driver.glusterfs_versions = {glusterfs_server: ('3', '7')} + + self.mock_object( + old_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), ('', '')])) + self.mock_object( + new_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), exception.ProcessExecutionError])) + self.mock_object(new_gmgr, 'get_gluster_vol_option', + mock.Mock()) + new_gmgr.get_gluster_vol_option.return_value = ( + 'glusterfs-server-1,client') + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + self.mock_object(self._driver, '_glustermanager', + mock.Mock(return_value=new_gmgr)) + + self.assertRaises(exception.GlusterfsException, + self._driver.create_share_from_snapshot, + self._context, share, snapshot) + + (self._driver._find_actual_backend_snapshot_name. + assert_called_once_with(old_gmgr, snapshot)) + args = (('snapshot', 'activate', 'fake_snap_id_xyz', + 'force', '--mode=script'), + ('snapshot', 'clone', volume, 'fake_snap_id_xyz')) + old_gmgr.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + self._driver._glustermanager.assert_called_once_with( + new_export_location) + new_gmgr.get_gluster_vol_option.assert_called_once_with( + 'auth.ssl-allow') + args = (('volume', 'set', new_gmgr.volume, 'auth.ssl-allow', + 'glusterfs-server-1'), + ('volume', 'start', new_gmgr.volume), ) + new_gmgr.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + self.assertNotIn(new_export_location, + self._driver.glusterfs_versions.keys()) + + def test_create_share_from_snapshot_error_unsupported_gluster_version( + self): + + glusterfs_target = 'root@host1:/gv1' + glusterfs_server = 'root@host1' + share = new_share() + snapshot = { + 'id': 'fake_snap_id', + 'share': new_share(export_location=glusterfs_target) + } + volume = ''.join(['manila-', share['id']]) + new_export_location = ':/'.join([glusterfs_server, volume]) + gmgr = glusterfs.GlusterManager + old_gmgr = gmgr(glusterfs_target, self._execute, None, None) + new_gmgr = gmgr(new_export_location, self._execute, None, None) + self._driver.gluster_used_vols_dict = {glusterfs_target: old_gmgr} + self._driver.glusterfs_versions = {glusterfs_server: ('3', '6')} + + self.mock_object( + old_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), ('', '')])) + self.mock_object( + new_gmgr, 'gluster_call', + mock.Mock(side_effect=[('', ''), exception.ProcessExecutionError])) + self.mock_object(new_gmgr, 'get_gluster_vol_option', + mock.Mock()) + new_gmgr.get_gluster_vol_option.return_value = ( + 'glusterfs-server-1,client') + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + self.mock_object(self._driver, '_glustermanager', + mock.Mock(return_value=new_gmgr)) + + self.assertRaises(exception.GlusterfsException, + self._driver.create_share_from_snapshot, + self._context, share, snapshot) + + self.assertFalse( + self._driver._find_actual_backend_snapshot_name.called) + self.assertFalse(old_gmgr.gluster_call.called) + self.assertFalse(self._driver._glustermanager.called) + self.assertFalse(new_gmgr.get_gluster_vol_option.called) + self.assertFalse(new_gmgr.gluster_call.called) + self.assertNotIn(new_export_location, + self._driver.glusterfs_versions.keys()) + def test_delete_snapshot(self): self._driver.gluster_nosnap_vols_dict = {} @@ -881,19 +1111,21 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): 'share_id': self.share1['id'], 'share': self.share1 } - - args = (('snapshot', 'list', gmgr1.volume, '--mode=script'), - ('--xml', 'snapshot', 'delete', 'fake_snap_id_xyz', - '--mode=script')) - self.mock_object(gmgr1, 'gluster_call', - mock.Mock(side_effect=(('fake_snap_id_xyz', ''), - GlusterXMLOut(ret=0, errno=0)()))) + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + args = ('--xml', 'snapshot', 'delete', 'fake_snap_id_xyz', + '--mode=script') + self.mock_object( + gmgr1, 'gluster_call', + mock.Mock(return_value=GlusterXMLOut(ret=0, errno=0)())) ret = self._driver.delete_snapshot(self._context, snapshot) self.assertEqual(None, ret) - gmgr1.gluster_call.assert_has_calls([mock.call(*a) for a in args]) + gmgr1.gluster_call.assert_called_once_with(*args) + (self._driver._find_actual_backend_snapshot_name. + assert_called_once_with(gmgr1, snapshot)) @ddt.data(GlusterXMLOut(ret=-1, errno=2)(), ('', '')) - def test_delete_snapshot_error(self, badxmlout): + def test_delete_snapshot_error(self, badxmloutput): self._driver.gluster_nosnap_vols_dict = {} gmgr = glusterfs.GlusterManager @@ -905,39 +1137,19 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): 'share_id': self.share1['id'], 'share': self.share1 } - - args = (('snapshot', 'list', gmgr1.volume, '--mode=script'), - ('--xml', 'snapshot', 'delete', 'fake_snap_id_xyz', - '--mode=script')) - self.mock_object(gmgr1, 'gluster_call', - mock.Mock(side_effect=(('fake_snap_id_xyz', ''), - badxmlout))) - self.assertRaises(exception.GlusterfsException, - self._driver.delete_snapshot, self._context, - snapshot) - gmgr1.gluster_call.assert_has_calls([mock.call(*a) for a in args]) - - @ddt.data('this is too bad', 'fake_snap_id_xyx\nfake_snap_id_pqr') - def test_delete_snapshot_bad_snap_list(self, snaplist): - self._driver.gluster_nosnap_vols_dict = {} - - gmgr = glusterfs.GlusterManager - gmgr1 = gmgr(self.share1['export_location'], self._execute, None, None) - - self._driver.gluster_used_vols_dict = {self.glusterfs_target1: gmgr1} - snapshot = { - 'id': 'fake_snap_id', - 'share_id': self.share1['id'], - 'share': self.share1 - } - - args = ('snapshot', 'list', gmgr1.volume, '--mode=script') - self.mock_object(gmgr1, 'gluster_call', - mock.Mock(side_effect=((snaplist, ''),))) + self.mock_object(self._driver, '_find_actual_backend_snapshot_name', + mock.Mock(return_value='fake_snap_id_xyz')) + args = ('--xml', 'snapshot', 'delete', 'fake_snap_id_xyz', + '--mode=script') + self.mock_object( + gmgr1, 'gluster_call', + mock.Mock(return_value=badxmloutput)) self.assertRaises(exception.GlusterfsException, self._driver.delete_snapshot, self._context, snapshot) gmgr1.gluster_call.assert_called_once_with(*args) + (self._driver._find_actual_backend_snapshot_name. + assert_called_once_with(gmgr1, snapshot)) def test_allow_access(self): self._driver._restart_gluster_vol = mock.Mock() @@ -1104,7 +1316,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): 'total_capacity_gb': 'infinite', 'free_capacity_gb': 'infinite', 'pools': None, - 'snapshot_support': False, + 'snapshot_support': True, } self._driver._update_share_stats()