]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Attach snapshot - driver only
authorXing Yang <xing.yang@emc.com>
Fri, 7 Aug 2015 18:49:30 +0000 (14:49 -0400)
committerXing Yang <xing.yang@emc.com>
Sat, 8 Aug 2015 15:44:46 +0000 (11:44 -0400)
This patch is a continuation of adding support for
non-disruptive backup. It provides a more efficient way
to backup an attached volume by creating a temp snapshot.
Since this is used internally for backup, the attach
snapshot interface is added in the driver only. For
drivers not implementing the attach snapshot interface,
backup will still be done using a volume.

Partial-implements blueprint non-disruptive-backup
Change-Id: I3649ef1d7c8a18f9d6ed0543d463354273d5f62a

cinder/db/sqlalchemy/migrate_repo/versions/052_add_provider_auth_column_to_snapshots.py [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/objects/snapshot.py
cinder/tests/unit/fake_driver.py
cinder/tests/unit/test_migrations.py
cinder/tests/unit/test_volume.py
cinder/tests/unit/utils.py
cinder/volume/driver.py
cinder/volume/drivers/lvm.py

diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/052_add_provider_auth_column_to_snapshots.py b/cinder/db/sqlalchemy/migrate_repo/versions/052_add_provider_auth_column_to_snapshots.py
new file mode 100644 (file)
index 0000000..dbfc231
--- /dev/null
@@ -0,0 +1,35 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from sqlalchemy import Column
+from sqlalchemy import MetaData, String, Table
+
+
+def upgrade(migrate_engine):
+    """Add provider_auth column to snapshots."""
+    meta = MetaData()
+    meta.bind = migrate_engine
+
+    snapshots = Table('snapshots', meta, autoload=True)
+    provider_auth = Column('provider_auth', String(255))
+    snapshots.create_column(provider_auth)
+    snapshots.update().values(provider_auth=None).execute()
+
+
+def downgrade(migrate_engine):
+    """Remove provider_auth column from snapshots."""
+    meta = MetaData()
+    meta.bind = migrate_engine
+
+    snapshots = Table('snapshots', meta, autoload=True)
+    provider_auth = snapshots.columns.provider_auth
+    snapshots.drop_column(provider_auth)
index 52639711ab188234f1a3dfa4aadbdf6f4bdf9932..3011c9463607ef72b8b22ec23559934926ffb275 100644 (file)
@@ -460,6 +460,7 @@ class Snapshot(BASE, CinderBase):
 
     provider_location = Column(String(255))
     provider_id = Column(String(255))
+    provider_auth = Column(String(255))
 
     volume = relationship(Volume, backref="snapshots",
                           foreign_keys=volume_id,
index 3e69d3eda5e62d484b7c62bdd2b864c169e1ffe9..c482313894cde4282c39645a08a77c08bd468855 100644 (file)
@@ -57,6 +57,7 @@ class Snapshot(base.CinderPersistentObject, base.CinderObject,
         'provider_location': fields.StringField(nullable=True),
         'provider_id': fields.UUIDField(nullable=True),
         'metadata': fields.DictOfStringsField(),
+        'provider_auth': fields.StringField(nullable=True),
 
         'volume': fields.ObjectField('Volume', nullable=True),
     }
index 286c4689843539190116e1ac4e7987d608e12d91..e0221702bcb450dfd3f086ee1f432ca03a507abc 100644 (file)
@@ -143,6 +143,18 @@ class LoggingVolumeDriver(driver.VolumeDriver):
     def terminate_connection(self, volume, connector):
         self.log_action('terminate_connection', volume)
 
+    def create_export_snapshot(self, context, snapshot):
+        self.log_action('create_export_snapshot', snapshot)
+
+    def remove_export_snapshot(self, context, snapshot):
+        self.log_action('remove_export_snapshot', snapshot)
+
+    def initialize_connection_snapshot(self, snapshot, connector):
+        self.log_action('initialize_connection_snapshot', snapshot)
+
+    def terminate_connection_snapshot(self, snapshot, connector):
+        self.log_action('terminate_connection_snapshot', snapshot)
+
     def create_cloned_volume(self, volume, src_vol):
         self.log_action('create_cloned_volume', volume)
 
index 589c68052ad369693f4d161119ef902a57325de0..f0e5d5677d1d16375b66e401a9bf189a3157f7b5 100644 (file)
@@ -856,6 +856,15 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
         consistencygroups = db_utils.get_table(engine, 'consistencygroups')
         self.assertNotIn('source_cgid', consistencygroups.c)
 
+    def _check_052(self, engine, data):
+        snapshots = db_utils.get_table(engine, 'snapshots')
+        self.assertIsInstance(snapshots.c.provider_auth.type,
+                              sqlalchemy.types.VARCHAR)
+
+    def _post_downgrade_052(self, engine):
+        snapshots = db_utils.get_table(engine, 'snapshots')
+        self.assertNotIn('provider_auth', snapshots.c)
+
     def test_walk_versions(self):
         self.walk_versions(True, False)
 
index bc6d5a52a0d3fde020953ec9df96ab24276873e5..eb815323fc108fd7c8293ae42c047f0857c61656 100644 (file)
@@ -5635,7 +5635,15 @@ class GenericVolumeDriverTestCase(DriverTestCase):
     """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'
@@ -5644,48 +5652,38 @@ class GenericVolumeDriverTestCase(DriverTestCase):
         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'
@@ -5700,7 +5698,7 @@ class GenericVolumeDriverTestCase(DriverTestCase):
         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
@@ -5716,8 +5714,75 @@ class GenericVolumeDriverTestCase(DriverTestCase):
         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)
@@ -6252,8 +6317,9 @@ class LVMVolumeDriverTestCase(DriverTestCase):
                                  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'
 
@@ -6270,7 +6336,7 @@ class LVMVolumeDriverTestCase(DriverTestCase):
         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')
@@ -6285,7 +6351,7 @@ class LVMVolumeDriverTestCase(DriverTestCase):
         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):
index 0f46fdec6cad97af1dd3c45022bf7aeea55bfd6d..c3e16123827480ed3c3e15a5c1f582e86d84ce8e 100644 (file)
@@ -39,6 +39,7 @@ def create_volume(ctxt,
                   replication_extended_status=None,
                   replication_driver_data=None,
                   consistencygroup_id=None,
+                  previous_status=None,
                   **kwargs):
     """Create a volume object in the DB."""
     vol = {}
@@ -61,6 +62,7 @@ def create_volume(ctxt,
     vol['replication_status'] = replication_status
     vol['replication_extended_status'] = replication_extended_status
     vol['replication_driver_data'] = replication_driver_data
+    vol['previous_status'] = previous_status
 
     return db.volume_create(ctxt, vol)
 
index f1489bcbc6e89ac2aa3380c7a3bb336b2b3ae2fe..9f4fda4706eb1d596b263fc81bca2272118ead47 100644 (file)
@@ -379,6 +379,49 @@ class BaseVD(object):
                 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
 
@@ -714,6 +757,64 @@ class BaseVD(object):
                 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
@@ -744,49 +845,112 @@ class BaseVD(object):
                     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."""
@@ -867,15 +1031,16 @@ class BaseVD(object):
                               {'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):
@@ -937,11 +1102,23 @@ class BaseVD(object):
         """
         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.
@@ -963,9 +1140,30 @@ class BaseVD(object):
         """
         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):
@@ -1366,15 +1564,27 @@ class VolumeDriver(ConsistencyGroupVD, TransferVD, ManageableVD, ExtendVD,
     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()
@@ -1780,9 +1990,18 @@ class FakeISCSIDriver(ISCSIDriver):
             '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."""
@@ -1824,10 +2043,22 @@ class FakeISCSIDriver(ISCSIDriver):
         """
         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.
index a78038dfbae94fe931267ba3b7e3e927dcfd324e..82ba49ecf805e8e7837dec07cc9ef7faef60cbbf 100644 (file)
@@ -488,7 +488,7 @@ class LVMVolumeDriver(driver.VolumeDriver):
                     backup_service.backup(backup, volume_file)
         finally:
             if temp_snapshot:
-                self._delete_snapshot(context, temp_snapshot)
+                self._delete_temp_snapshot(context, temp_snapshot)
                 backup.temp_snapshot_id = None
                 backup.save()