"""Test case for VolumeDriver."""
driver_name = "cinder.tests.unit.fake_driver.LoggingVolumeDriver"
- def test_backup_volume(self):
+ @mock.patch.object(utils, 'temporary_chown')
+ @mock.patch.object(fileutils, 'file_open')
+ @mock.patch.object(os_brick.initiator.connector,
+ 'get_connector_properties')
+ @mock.patch.object(db, 'volume_get')
+ def test_backup_volume_available(self, mock_volume_get,
+ mock_get_connector_properties,
+ mock_file_open,
+ mock_temporary_chown):
vol = tests_utils.create_volume(self.context)
self.context.user_id = 'fake'
self.context.project_id = 'fake'
backup_obj = objects.Backup.get_by_id(self.context, backup.id)
properties = {}
attach_info = {'device': {'path': '/dev/null'}}
- backup_service = self.mox.CreateMock(backup_driver.BackupDriver)
- root_helper = 'sudo cinder-rootwrap /etc/cinder/rootwrap.conf'
- self.mox.StubOutWithMock(self.volume.driver.db, 'volume_get')
- self.mox.StubOutWithMock(os_brick.initiator.connector,
- 'get_connector_properties')
- self.mox.StubOutWithMock(self.volume.driver, '_attach_volume')
- self.mox.StubOutWithMock(os, 'getuid')
- self.mox.StubOutWithMock(utils, 'execute')
- self.mox.StubOutWithMock(fileutils, 'file_open')
- self.mox.StubOutWithMock(self.volume.driver, '_detach_volume')
- self.mox.StubOutWithMock(self.volume.driver, 'terminate_connection')
+ backup_service = mock.Mock()
+
+ self.volume.driver._attach_volume = mock.MagicMock()
+ self.volume.driver._detach_volume = mock.MagicMock()
+ self.volume.driver.terminate_connection = mock.MagicMock()
+ self.volume.driver.create_snapshot = mock.MagicMock()
+ self.volume.driver.delete_snapshot = mock.MagicMock()
+
+ mock_volume_get.return_value = vol
+ mock_get_connector_properties.return_value = properties
+ f = mock_file_open.return_value = file('/dev/null')
+
+ backup_service.backup(backup_obj, f, None)
+ self.volume.driver._attach_volume.return_value = attach_info, vol
- self.volume.driver.db.volume_get(self.context, vol['id']).\
- AndReturn(vol)
- os_brick.initiator.connector.\
- get_connector_properties(root_helper, CONF.my_ip, False, False).\
- AndReturn(properties)
- self.volume.driver._attach_volume(self.context, vol, properties).\
- AndReturn((attach_info, vol))
- os.getuid()
- utils.execute('chown', None, '/dev/null', run_as_root=True)
- f = fileutils.file_open('/dev/null').AndReturn(file('/dev/null'))
- backup_service.backup(backup_obj, f)
- utils.execute('chown', 0, '/dev/null', run_as_root=True)
- self.volume.driver._detach_volume(self.context, attach_info, vol,
- properties)
- self.mox.ReplayAll()
self.volume.driver.backup_volume(self.context, backup_obj,
backup_service)
- self.mox.UnsetStubs()
+
+ mock_volume_get.assert_called_with(self.context, vol['id'])
@mock.patch.object(utils, 'temporary_chown')
@mock.patch.object(fileutils, 'file_open')
@mock.patch.object(os_brick.initiator.connector,
'get_connector_properties')
@mock.patch.object(db, 'volume_get')
- def test_backup_volume_inuse(self, mock_volume_get,
- mock_get_connector_properties,
- mock_file_open,
- mock_temporary_chown):
- vol = tests_utils.create_volume(self.context)
- vol['previous_status'] = 'in-use'
+ def test_backup_volume_inuse_temp_volume(self, mock_volume_get,
+ mock_get_connector_properties,
+ mock_file_open,
+ mock_temporary_chown):
+ vol = tests_utils.create_volume(self.context,
+ status='backing-up',
+ previous_status='in-use')
temp_vol = tests_utils.create_volume(self.context)
self.context.user_id = 'fake'
self.context.project_id = 'fake'
self.volume.driver._detach_volume = mock.MagicMock()
self.volume.driver.terminate_connection = mock.MagicMock()
self.volume.driver._create_temp_cloned_volume = mock.MagicMock()
- self.volume.driver._delete_volume = mock.MagicMock()
+ self.volume.driver._delete_temp_volume = mock.MagicMock()
mock_volume_get.return_value = vol
self.volume.driver._create_temp_cloned_volume.return_value = temp_vol
mock_volume_get.assert_called_with(self.context, vol['id'])
self.volume.driver._create_temp_cloned_volume.assert_called_once_with(
self.context, vol)
- self.volume.driver._delete_volume.assert_called_once_with(self.context,
- temp_vol)
+ self.volume.driver._delete_temp_volume.assert_called_once_with(
+ self.context, temp_vol)
+
+ @mock.patch.object(cinder.volume.driver.VolumeDriver,
+ 'backup_use_temp_snapshot',
+ return_value=True)
+ @mock.patch.object(utils, 'temporary_chown')
+ @mock.patch.object(fileutils, 'file_open')
+ @mock.patch.object(os_brick.initiator.connector.LocalConnector,
+ 'connect_volume')
+ @mock.patch.object(os_brick.initiator.connector.LocalConnector,
+ 'check_valid_device',
+ return_value=True)
+ @mock.patch.object(os_brick.initiator.connector,
+ 'get_connector_properties',
+ return_value={})
+ @mock.patch.object(db, 'volume_get')
+ def test_backup_volume_inuse_temp_snapshot(self, mock_volume_get,
+ mock_get_connector_properties,
+ mock_check_device,
+ mock_connect_volume,
+ mock_file_open,
+ mock_temporary_chown,
+ mock_temp_snapshot):
+ vol = tests_utils.create_volume(self.context,
+ status='backing-up',
+ previous_status='in-use')
+ self.context.user_id = 'fake'
+ self.context.project_id = 'fake'
+ backup = tests_utils.create_backup(self.context,
+ vol['id'])
+ backup_obj = objects.Backup.get_by_id(self.context, backup.id)
+ attach_info = {'device': {'path': '/dev/null'},
+ 'driver_volume_type': 'LOCAL',
+ 'data': {}}
+ backup_service = mock.Mock()
+
+ self.volume.driver.terminate_connection_snapshot = mock.MagicMock()
+ self.volume.driver.initialize_connection_snapshot = mock.MagicMock()
+ self.volume.driver.create_snapshot = mock.MagicMock()
+ self.volume.driver.delete_snapshot = mock.MagicMock()
+ self.volume.driver.create_export_snapshot = mock.MagicMock()
+ self.volume.driver.remove_export_snapshot = mock.MagicMock()
+
+ mock_volume_get.return_value = vol
+ mock_connect_volume.return_value = {'type': 'local',
+ 'path': '/dev/null'}
+ f = mock_file_open.return_value = file('/dev/null')
+
+ self.volume.driver._connect_device
+ backup_service.backup(backup_obj, f, None)
+ self.volume.driver.initialize_connection_snapshot.return_value = (
+ attach_info)
+ self.volume.driver.create_export_snapshot.return_value = (
+ {'provider_location': '/dev/null',
+ 'provider_auth': 'xxxxxxxx'})
+
+ self.volume.driver.backup_volume(self.context, backup_obj,
+ backup_service)
+
+ mock_volume_get.assert_called_with(self.context, vol['id'])
+ self.assertTrue(self.volume.driver.create_snapshot.called)
+ self.assertTrue(self.volume.driver.create_export_snapshot.called)
+ self.assertTrue(
+ self.volume.driver.initialize_connection_snapshot.called)
+ self.assertTrue(
+ self.volume.driver.terminate_connection_snapshot.called)
+ self.assertTrue(self.volume.driver.remove_export_snapshot.called)
+ self.assertTrue(self.volume.driver.delete_snapshot.called)
def test_restore_backup(self):
vol = tests_utils.create_volume(self.context)
mock_file_open,
mock_temporary_chown):
- vol = tests_utils.create_volume(self.context)
- vol['previous_status'] = 'in-use'
+ vol = tests_utils.create_volume(self.context,
+ status='backing-up',
+ previous_status='in-use')
self.context.user_id = 'fake'
self.context.project_id = 'fake'
self.volume.driver._attach_volume = mock.MagicMock()
self.volume.driver.terminate_connection = mock.MagicMock()
self.volume.driver._create_temp_snapshot = mock.MagicMock()
- self.volume.driver._delete_snapshot = mock.MagicMock()
+ self.volume.driver._delete_temp_snapshot = mock.MagicMock()
mock_get_connector_properties.return_value = properties
f = mock_file_open.return_value = file('/dev/null')
mock_volume_get.assert_called_with(self.context, vol['id'])
self.volume.driver._create_temp_snapshot.assert_called_once_with(
self.context, vol)
- self.volume.driver._delete_snapshot.assert_called_once_with(
+ self.volume.driver._delete_temp_snapshot.assert_called_once_with(
self.context, temp_snapshot)
def test_create_volume_from_snapshot_none_sparse(self):
raise exception.RemoveExportException(volume=volume['id'],
reason=ex)
+ def _detach_snapshot(self, context, attach_info, snapshot, properties,
+ force=False, remote=False):
+ """Disconnect the snapshot from the host."""
+ # Use Brick's code to do attach/detach
+ connector = attach_info['connector']
+ connector.disconnect_volume(attach_info['conn']['data'],
+ attach_info['device'])
+
+ # NOTE(xyang): This method is introduced for non-disruptive backup.
+ # Currently backup service has to be on the same node as the volume
+ # driver. Therefore it is not possible to call a volume driver on a
+ # remote node. In the future, if backup can be done from a remote
+ # node, this function can be modified to allow RPC calls. The remote
+ # flag in the interface is for anticipation that it will be enabled
+ # in the future.
+ if remote:
+ LOG.exception(_LE("Detaching snapshot from a remote node "
+ "is not supported."))
+ raise exception.NotSupportedOperation(
+ operation=_("detach snapshot from remote node"))
+ else:
+ # Call local driver's terminate_connection and remove export.
+ # NOTE(avishay) This is copied from the manager's code - need to
+ # clean this up in the future.
+ try:
+ self.terminate_connection_snapshot(snapshot, properties,
+ force=force)
+ except Exception as err:
+ err_msg = (_('Unable to terminate volume connection: %(err)s')
+ % {'err': six.text_type(err)})
+ LOG.error(err_msg)
+ raise exception.VolumeBackendAPIException(data=err_msg)
+
+ try:
+ LOG.debug("Snapshot %s: removing export.", snapshot.id)
+ self.remove_export_snapshot(context, snapshot)
+ except Exception as ex:
+ LOG.exception(_LE("Error detaching snapshot %(snapshot)s, "
+ "due to remove export failure."),
+ {"snapshot": snapshot.id})
+ raise exception.RemoveExportException(volume=snapshot.id,
+ reason=ex)
+
def set_execute(self, execute):
self._execute = execute
raise exception.VolumeBackendAPIException(data=err_msg)
return (self._connect_device(conn), volume)
+ def _attach_snapshot(self, context, snapshot, properties, remote=False):
+ """Attach the snapshot."""
+ # NOTE(xyang): This method is introduced for non-disruptive backup.
+ # Currently backup service has to be on the same node as the volume
+ # driver. Therefore it is not possible to call a volume driver on a
+ # remote node. In the future, if backup can be done from a remote
+ # node, this function can be modified to allow RPC calls. The remote
+ # flag in the interface is for anticipation that it will be enabled
+ # in the future.
+ if remote:
+ LOG.exception(_LE("Attaching snapshot from a remote node "
+ "is not supported."))
+ raise exception.NotSupportedOperation(
+ operation=_("attach snapshot from remote node"))
+ else:
+ # Call local driver's create_export and initialize_connection.
+ # NOTE(avishay) This is copied from the manager's code - need to
+ # clean this up in the future.
+ model_update = None
+ try:
+ LOG.debug("Snapshot %s: creating export.", snapshot.id)
+ model_update = self.create_export_snapshot(context, snapshot,
+ properties)
+ if model_update:
+ snapshot.provider_location = model_update.get(
+ 'provider_location', None)
+ snapshot.provider_auth = model_update.get(
+ 'provider_auth', None)
+ snapshot.save()
+ except exception.CinderException as ex:
+ if model_update:
+ LOG.exception(_LE("Failed updating model of snapshot "
+ "%(snapshot_id)s with driver provided "
+ "model %(model)s."),
+ {'snapshot_id': snapshot.id,
+ 'model': model_update})
+ raise exception.ExportFailure(reason=ex)
+
+ try:
+ conn = self.initialize_connection_snapshot(
+ snapshot, properties)
+ except Exception as err:
+ try:
+ err_msg = (_('Unable to fetch connection information from '
+ 'backend: %(err)s') %
+ {'err': six.text_type(err)})
+ LOG.error(err_msg)
+ LOG.debug("Cleaning up failed connect initialization.")
+ self.remove_export_snapshot(context, snapshot)
+ except Exception as ex:
+ ex_msg = (_('Error encountered during cleanup '
+ 'of a failed attach: %(ex)s') %
+ {'ex': six.text_type(ex)})
+ LOG.error(err_msg)
+ raise exception.VolumeBackendAPIException(data=ex_msg)
+ raise exception.VolumeBackendAPIException(data=err_msg)
+ return (self._connect_device(conn), snapshot)
+
def _connect_device(self, conn):
# Use Brick's code to do attach/detach
use_multipath = self.configuration.use_multipath_for_image_xfer
image_service):
return None, False
+ def backup_use_temp_snapshot(self):
+ return False
+
def backup_volume(self, context, backup, backup_service):
"""Create a new backup from an existing volume."""
+ if self.backup_use_temp_snapshot():
+ self._backup_volume_temp_snapshot(context, backup,
+ backup_service)
+ else:
+ self._backup_volume_temp_volume(context, backup,
+ backup_service)
+
+ def _backup_volume_temp_volume(self, context, backup, backup_service):
+ """Create a new backup from an existing volume.
+
+ For in-use volume, create a temp volume and back it up.
+ """
volume = self.db.volume_get(context, backup.volume_id)
LOG.debug('Creating a new backup for volume %s.', volume['name'])
- # NOTE(xyang): Check volume status; if not 'available', create a
- # a temp volume from the volume, and backup the temp volume, and
+ # NOTE(xyang): Check volume status; if 'in-use', create a temp
+ # volume from the source volume, backup the temp volume, and
# then clean up the temp volume; if 'available', just backup the
# volume.
previous_status = volume.get('previous_status', None)
+ device_to_backup = volume
temp_vol_ref = None
if previous_status == "in-use":
temp_vol_ref = self._create_temp_cloned_volume(
context, volume)
backup.temp_volume_id = temp_vol_ref['id']
backup.save()
- volume = temp_vol_ref
+ device_to_backup = temp_vol_ref
+
+ self._backup_device(context, backup, backup_service, device_to_backup)
+
+ if temp_vol_ref:
+ self._delete_temp_volume(context, temp_vol_ref)
+ backup.temp_volume_id = None
+ backup.save()
+
+ def _backup_volume_temp_snapshot(self, context, backup, backup_service):
+ """Create a new backup from an existing volume.
+
+ For in-use volume, create a temp snapshot and back it up.
+ """
+ volume = self.db.volume_get(context, backup.volume_id)
+
+ LOG.debug('Creating a new backup for volume %s.', volume['name'])
+
+ # NOTE(xyang): Check volume status; if 'in-use', create a temp
+ # snapshot from the source volume, backup the temp snapshot, and
+ # then clean up the temp snapshot; if 'available', just backup the
+ # volume.
+ previous_status = volume.get('previous_status', None)
+ device_to_backup = volume
+ is_snapshot = False
+ temp_snapshot = None
+ if previous_status == "in-use":
+ temp_snapshot = self._create_temp_snapshot(context, volume)
+ backup.temp_snapshot_id = temp_snapshot.id
+ backup.save()
+ device_to_backup = temp_snapshot
+ is_snapshot = True
+ self._backup_device(context, backup, backup_service, device_to_backup,
+ is_snapshot)
+
+ if temp_snapshot:
+ self._delete_temp_snapshot(context, temp_snapshot)
+ backup.temp_snapshot_id = None
+ backup.save()
+
+ def _backup_device(self, context, backup, backup_service, device,
+ is_snapshot=False):
+ """Create a new backup from a volume or snapshot."""
+
+ LOG.debug('Creating a new backup for %s.', device['name'])
use_multipath = self.configuration.use_multipath_for_image_xfer
enforce_multipath = self.configuration.enforce_multipath_for_image_xfer
properties = utils.brick_get_connector_properties(use_multipath,
enforce_multipath)
- attach_info, volume = self._attach_volume(context, volume, properties)
-
+ if is_snapshot:
+ attach_info, device = self._attach_snapshot(context, device,
+ properties)
+ else:
+ attach_info, device = self._attach_volume(context, device,
+ properties)
try:
- volume_path = attach_info['device']['path']
+ device_path = attach_info['device']['path']
# Secure network file systems will not chown files.
if self.secure_file_operations_enabled():
- with fileutils.file_open(volume_path) as volume_file:
- backup_service.backup(backup, volume_file)
+ with fileutils.file_open(device_path) as device_file:
+ backup_service.backup(backup, device_file)
else:
- with utils.temporary_chown(volume_path):
- with fileutils.file_open(volume_path) as volume_file:
- backup_service.backup(backup, volume_file)
+ with utils.temporary_chown(device_path):
+ with fileutils.file_open(device_path) as device_file:
+ backup_service.backup(backup, device_file)
finally:
- self._detach_volume(context, attach_info, volume, properties)
- if temp_vol_ref:
- self._delete_volume(context, temp_vol_ref)
- backup.temp_volume_id = None
- backup.save()
+ if is_snapshot:
+ self._detach_snapshot(context, attach_info, device, properties)
+ else:
+ self._detach_volume(context, attach_info, device, properties)
def restore_backup(self, context, backup, volume, backup_service):
"""Restore an existing backup to a new or existing volume."""
{'status': 'available'})
return temp_vol_ref
- def _delete_snapshot(self, context, snapshot):
+ def _delete_temp_snapshot(self, context, snapshot):
self.delete_snapshot(snapshot)
with snapshot.obj_as_admin():
self.db.volume_glance_metadata_delete_by_snapshot(
context, snapshot.id)
snapshot.destroy()
- def _delete_volume(self, context, volume):
+ def _delete_temp_volume(self, context, volume):
self.delete_volume(volume)
+ context = context.elevated()
self.db.volume_destroy(context, volume['id'])
def clear_download(self, context, volume):
"""
return
+ def create_export_snapshot(self, context, snapshot, connector):
+ """Exports the snapshot.
+
+ Can optionally return a Dictionary of changes
+ to the snapshot object to be persisted.
+ """
+ return
+
@abc.abstractmethod
def remove_export(self, context, volume):
"""Removes an export for a volume."""
return
+ def remove_export_snapshot(self, context, snapshot):
+ """Removes an export for a snapshot."""
+ return
+
@abc.abstractmethod
def initialize_connection(self, volume, connector, initiator_data=None):
"""Allow connection to connector and return connection info.
"""
return
+ def initialize_connection_snapshot(self, snapshot, connector, **kwargs):
+ """Allow connection to connector and return connection info.
+
+ :param snapshot: The snapshot to be attached
+ :param connector: Dictionary containing information about what is being
+ connected to.
+ :returns conn_info: A dictionary of connection information. This can
+ optionally include a "initiator_updates" field.
+
+ The "initiator_updates" field must be a dictionary containing a
+ "set_values" and/or "remove_values" field. The "set_values" field must
+ be a dictionary of key-value pairs to be set/updated in the db. The
+ "remove_values" field must be a list of keys, previously set with
+ "set_values", that will be deleted from the db.
+ """
+ return
+
@abc.abstractmethod
def terminate_connection(self, volume, connector, **kwargs):
- """Disallow connection from connector"""
+ """Disallow connection from connector."""
+ return
+
+ def terminate_connection_snapshot(self, snapshot, connector, **kwargs):
+ """Disallow connection from connector."""
return
def get_pool(self, volume):
def create_export(self, context, volume, connector):
raise NotImplementedError()
+ def create_export_snapshot(self, context, snapshot, connector):
+ raise NotImplementedError()
+
def remove_export(self, context, volume):
raise NotImplementedError()
+ def remove_export_snapshot(self, context, snapshot):
+ raise NotImplementedError()
+
def initialize_connection(self, volume, connector):
raise NotImplementedError()
+ def initialize_connection_snapshot(self, snapshot, connector, **kwargs):
+ """Allow connection from connector for a snapshot."""
+
def terminate_connection(self, volume, connector, **kwargs):
"""Disallow connection from connector"""
+ def terminate_connection_snapshot(self, snapshot, connector, **kwargs):
+ """Disallow connection from connector for a snapshot."""
+
def create_consistencygroup(self, context, group):
"""Creates a consistencygroup."""
raise NotImplementedError()
'discard': False,
}
+ def initialize_connection_snapshot(self, snapshot, connector):
+ return {
+ 'driver_volume_type': 'iscsi',
+ 'data': {'access_mode': 'rw'}
+ }
+
def terminate_connection(self, volume, connector, **kwargs):
pass
+ def terminate_connection_snapshot(self, snapshot, connector, **kwargs):
+ pass
+
@staticmethod
def fake_execute(cmd, *_args, **_kwargs):
"""Execute that simply logs the command."""
"""
pass
+ def create_export_snapshot(self, context, snapshot, connector):
+ """Exports the snapshot.
+
+ Can optionally return a Dictionary of changes to the snapshot object to
+ be persisted.
+ """
+ pass
+
def remove_export(self, context, volume):
"""Removes an export for a volume."""
pass
+ def remove_export_snapshot(self, context, snapshot):
+ """Removes an export for a snapshot."""
+ pass
+
class ISERDriver(ISCSIDriver):
"""Executes commands relating to ISER volumes.