# Copyright 2013 Canonical Ltd.
# All Rights Reserved.
#
#    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.
""" Tests for Ceph backup service."""

import hashlib
import json
import os
import subprocess
import tempfile
import threading
from unittest import mock

import ddt
from os_brick.initiator import linuxrbd
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import units

from cinder.backup import driver
from cinder.backup.drivers import ceph
from cinder import context
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import test
import cinder.volume.drivers.rbd as rbd_driver

# This is used to collect raised exceptions so that tests may check what was
# raised.
# NOTE: this must be initialised in test setUp().
RAISED_EXCEPTIONS = []

CONF = cfg.CONF


class MockException(Exception):

    def __init__(self, *args, **kwargs):
        RAISED_EXCEPTIONS.append(self.__class__)


class MockImageNotFoundException(MockException):
    """Used as mock for rbd.ImageNotFound."""


class MockImageBusyException(MockException):
    """Used as mock for rbd.ImageBusy."""


class MockObjectNotFoundException(MockException):
    """Used as mock for rados.MockObjectNotFoundException."""


def common_mocks(f):
    """Decorator to set mocks common to all tests.

    The point of doing these mocks here is so that we don't accidentally set
    mocks that can't/don't get unset.
    """
    def _common_inner_inner1(inst, *args, **kwargs):
        # NOTE(dosaboy): mock Popen to, by default, raise Exception in order to
        #                ensure that any test ending up in a subprocess fails
        #                if not properly mocked.
        @mock.patch('subprocess.Popen', spec=True)
        # NOTE(dosaboy): mock out eventlet.sleep() so that it does nothing.
        @mock.patch('eventlet.sleep', spec=True)
        @mock.patch('time.time', spec=True)
        # NOTE(dosaboy): set spec to empty object so that hasattr calls return
        #                False by default.
        @mock.patch('cinder.backup.drivers.ceph.rbd')
        @mock.patch('cinder.backup.drivers.ceph.rados')
        def _common_inner_inner2(mock_rados, mock_rbd, mock_time, mock_sleep,
                                 mock_popen):
            mock_time.side_effect = inst.time_inc
            mock_popen.side_effect = Exception

            inst.mock_rados = mock_rados
            inst.mock_rbd = mock_rbd
            inst.mock_rbd.ImageBusy = MockImageBusyException
            inst.mock_rbd.ImageNotFound = MockImageNotFoundException
            inst.mock_rados.ObjectNotFound = MockObjectNotFoundException

            inst.service.rbd = inst.mock_rbd
            inst.service.rados = inst.mock_rados
            return f(inst, *args, **kwargs)

        return _common_inner_inner2()

    return _common_inner_inner1


@ddt.ddt
class BackupCephTestCase(test.TestCase):
    """Test case for ceph backup driver."""

    def _create_volume_db_entry(self, id, size):
        vol = {'id': id, 'size': size, 'status': 'available',
               'volume_type_id': self.vt['id']}
        return db.volume_create(self.ctxt, vol)['id']

    def _create_backup_db_entry(self, backupid, volid, size,
                                userid=fake.USER_ID,
                                projectid=fake.PROJECT_ID):
        backup = {'id': backupid, 'size': size, 'volume_id': volid,
                  'user_id': userid, 'project_id': projectid}
        return db.backup_create(self.ctxt, backup)['id']

    def _create_parent_backup_object(self):
        tmp_backup_id = fake.BACKUP3_ID
        self._create_backup_db_entry(tmp_backup_id, self.volume_id,
                                     self.volume_size)
        tmp_backup = objects.Backup.get_by_id(self.ctxt, tmp_backup_id)
        tmp_backup.service_metadata = 'mock_base_name'
        return tmp_backup

    def time_inc(self):
        self.counter += 1
        return self.counter

    def _get_wrapped_rbd_io(self, rbd_image):
        rbd_meta = linuxrbd.RBDImageMetadata(rbd_image, 'pool_foo',
                                             'user_foo', 'conf_foo')
        return linuxrbd.RBDVolumeIOWrapper(rbd_meta)

    def _setup_mock_popen(self, retval=None, p1hook=None, p2hook=None):

        class MockPopen(object):
            hooks = [p2hook, p1hook]

            def __init__(mock_inst, cmd, *args, **kwargs):
                self.callstack.append('popen_init')
                mock_inst.stdout = mock.Mock()
                mock_inst.stdout.close = mock.Mock()
                mock_inst.stdout.close.side_effect = \
                    lambda *args: self.callstack.append('stdout_close')
                mock_inst.returncode = 0
                hook = mock_inst.__class__.hooks.pop()
                if hook is not None:
                    hook()

            def communicate(mock_inst):
                self.callstack.append('communicate')
                return retval

            def wait(mock_inst):
                self.callstack.append('wait')
                return retval

        subprocess.Popen.side_effect = MockPopen

    def setUp(self):
        global RAISED_EXCEPTIONS
        RAISED_EXCEPTIONS = []
        super(BackupCephTestCase, self).setUp()
        self.ctxt = context.get_admin_context()

        # Create volume.
        self.volume_size = 1
        self.volume_id = fake.VOLUME_ID
        self._create_volume_db_entry(self.volume_id, self.volume_size)
        self.volume = db.volume_get(self.ctxt, self.volume_id)

        # Create backup of volume.
        self.backup_id = fake.BACKUP_ID
        self._create_backup_db_entry(self.backup_id, self.volume_id,
                                     self.volume_size)
        self.backup = objects.Backup.get_by_id(self.ctxt, self.backup_id)
        self.backup.container = "backups"

        # Create parent backup of volume
        self.parent_backup = self._create_parent_backup_object()

        # Create alternate backup with parent
        self.alt_backup_id = fake.BACKUP2_ID
        self._create_backup_db_entry(self.alt_backup_id, self.volume_id,
                                     self.volume_size)

        self.alt_backup = objects.Backup.get_by_id(self.ctxt,
                                                   self.alt_backup_id)

        base_name = "volume-%s.backup.%s" % (self.volume_id, self.backup_id)
        self.alt_backup.container = "backups"
        self.alt_backup.parent = self.backup
        self.alt_backup.parent.service_metadata = '{"base": "%s"}' % base_name

        # Create alternate volume.
        self.alt_volume_id = fake.VOLUME2_ID
        self._create_volume_db_entry(self.alt_volume_id, self.volume_size)
        self.alt_volume = db.volume_get(self.ctxt, self.alt_volume_id)

        self.chunk_size = 1024
        self.num_chunks = 128
        self.data_length = self.num_chunks * self.chunk_size
        self.checksum = hashlib.sha256()

        # Create a file with some data in it.
        self.volume_file = tempfile.NamedTemporaryFile()
        self.addCleanup(self.volume_file.close)
        for _i in range(0, self.num_chunks):
            data = os.urandom(self.chunk_size)
            self.checksum.update(data)
            self.volume_file.write(data)

        self.volume_file.seek(0)

        # Always trigger an exception if a command is executed since it should
        # always be dealt with gracefully. At time of writing on rbd
        # export/import-diff is executed and if they fail we expect to find
        # alternative means of backing up.
        mock_exec = mock.Mock()
        mock_exec.side_effect = processutils.ProcessExecutionError

        self.service = ceph.CephBackupDriver(self.ctxt, execute=mock_exec)

        # Ensure that time.time() always returns more than the last time it was
        # called to avoid div by zero errors.
        self.counter = float(0)

        self.callstack = []

    @common_mocks
    def test_get_rbd_support(self):
        del self.service.rbd.RBD_FEATURE_LAYERING
        del self.service.rbd.RBD_FEATURE_STRIPINGV2
        del self.service.rbd.RBD_FEATURE_EXCLUSIVE_LOCK
        del self.service.rbd.RBD_FEATURE_JOURNALING
        del self.service.rbd.RBD_FEATURE_OBJECT_MAP
        del self.service.rbd.RBD_FEATURE_FAST_DIFF
        self.assertFalse(hasattr(self.service.rbd, 'RBD_FEATURE_LAYERING'))
        self.assertFalse(hasattr(self.service.rbd, 'RBD_FEATURE_STRIPINGV2'))
        self.assertFalse(hasattr(self.service.rbd,
                                 'RBD_FEATURE_EXCLUSIVE_LOCK'))
        self.assertFalse(hasattr(self.service.rbd, 'RBD_FEATURE_JOURNALING'))
        self.assertFalse(hasattr(self.service.rbd, 'RBD_FEATURE_OBJECT_MAP'))
        self.assertFalse(hasattr(self.service.rbd, 'RBD_FEATURE_FAST_DIFF'))

        oldformat, features = self.service._get_rbd_support()
        self.assertTrue(oldformat)
        self.assertEqual(0, features)

        self.service.rbd.RBD_FEATURE_LAYERING = 1

        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1, features)

        self.service.rbd.RBD_FEATURE_STRIPINGV2 = 2

        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1 | 2, features)

        # initially, backup_ceph_image_journals = False. test that
        #   the flags are defined, but that they are not returned.
        self.service.rbd.RBD_FEATURE_EXCLUSIVE_LOCK = 4

        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1 | 2, features)

        self.service.rbd.RBD_FEATURE_JOURNALING = 64

        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1 | 2, features)

        # test that the config setting properly sets the FEATURE bits.
        #   because journaling requires exclusive-lock, these are set
        #   at the same time.
        CONF.set_override("backup_ceph_image_journals", True)
        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1 | 2 | 4 | 64, features)

        #
        # test that FAST_DIFF is enabled if supported by RBD
        #   this also enables OBJECT_MAP as required by Ceph
        #
        self.service.rbd.RBD_FEATURE_OBJECT_MAP = 8
        self.service.rbd.RBD_FEATURE_FAST_DIFF = 16

        oldformat, features = self.service._get_rbd_support()
        self.assertFalse(oldformat)
        self.assertEqual(1 | 2 | 4 | 8 | 16 | 64, features)

    @common_mocks
    def test_get_backup_snap_name(self):
        snap_name = 'backup.%s.snap.3824923.1412' % (fake.VOLUME3_ID)

        def get_backup_snaps(inst, *args):
            return [{'name': 'backup.%s.snap.6423868.2342' % (fake.UUID1),
                     'backup_id': fake.BACKUP2_ID},
                    {'name': snap_name,
                     'backup_id': self.backup_id}]

        with mock.patch.object(self.service, 'get_backup_snaps'):
            name = self.service._get_backup_snap_name(self.service.rbd.Image(),
                                                      'base_foo',
                                                      self.backup_id)
            self.assertIsNone(name)

        with mock.patch.object(self.service, 'get_backup_snaps') as \
                mock_get_backup_snaps:
            mock_get_backup_snaps.side_effect = get_backup_snaps
            name = self.service._get_backup_snap_name(self.service.rbd.Image(),
                                                      'base_foo',
                                                      self.backup_id)
            self.assertEqual(snap_name, name)
            self.assertTrue(mock_get_backup_snaps.called)

    @common_mocks
    def test_get_backup_snaps(self):
        image = self.mock_rbd.Image.return_value
        image.list_snaps.return_value = [
            {'name': 'backup.%s.snap.6423868.2342' % (fake.UUID1)},
            {'name': 'backup.%s.wambam.6423868.2342' % (fake.UUID2)},
            {'name': 'backup.%s.snap.1321319.3235' % (fake.UUID3)},
            {'name': 'bbbackup.%s.snap.1321319.3235' % (fake.UUID4)},
            {'name': 'backup.%s.snap.3824923.1412' % (fake.UUID5)}]
        snaps = self.service.get_backup_snaps(image)
        self.assertEqual(3, len(snaps))

    @common_mocks
    def test_transfer_data_from_rbd_to_file(self):
        def fake_read(offset, length):
            self.volume_file.seek(offset)
            return self.volume_file.read(length)

        self.mock_rbd.Image.return_value.read.side_effect = fake_read
        self.mock_rbd.Image.return_value.size.return_value = self.data_length

        with tempfile.NamedTemporaryFile() as test_file:
            self.volume_file.seek(0)

            rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
            self.service._transfer_data(rbd_io, 'src_foo', test_file,
                                        'dest_foo', self.data_length)

            checksum = hashlib.sha256()
            test_file.seek(0)
            for _c in range(0, self.num_chunks):
                checksum.update(test_file.read(self.chunk_size))

            # Ensure the files are equal
            self.assertEqual(checksum.digest(), self.checksum.digest())

    @common_mocks
    def test_transfer_data_from_rbd_to_rbd(self):
        def fake_read(offset, length):
            self.volume_file.seek(offset)
            return self.volume_file.read(length)

        def mock_write_data(data, offset):
            checksum.update(data)
            test_file.write(data)

        rbd1 = mock.Mock()
        rbd1.read.side_effect = fake_read
        rbd1.size.return_value = os.fstat(self.volume_file.fileno()).st_size

        rbd2 = mock.Mock()
        rbd2.write.side_effect = mock_write_data

        with tempfile.NamedTemporaryFile() as test_file:
            self.volume_file.seek(0)
            checksum = hashlib.sha256()

            src_rbd_io = self._get_wrapped_rbd_io(rbd1)
            dest_rbd_io = self._get_wrapped_rbd_io(rbd2)
            self.service._transfer_data(src_rbd_io, 'src_foo', dest_rbd_io,
                                        'dest_foo', self.data_length)

            # Ensure the files are equal
            self.assertEqual(checksum.digest(), self.checksum.digest())

    @common_mocks
    def test_transfer_data_from_file_to_rbd(self):

        def mock_write_data(data, offset):
            checksum.update(data)
            test_file.write(data)

        self.mock_rbd.Image.return_value.write.side_effect = mock_write_data

        with tempfile.NamedTemporaryFile() as test_file:
            self.volume_file.seek(0)
            checksum = hashlib.sha256()

            rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
            self.service._transfer_data(self.volume_file, 'src_foo',
                                        rbd_io, 'dest_foo', self.data_length)

            # Ensure the files are equal
            self.assertEqual(checksum.digest(), self.checksum.digest())

    @common_mocks
    def test_transfer_data_from_file_to_file(self):
        with tempfile.NamedTemporaryFile() as test_file:
            self.volume_file.seek(0)
            checksum = hashlib.sha256()

            self.service._transfer_data(self.volume_file, 'src_foo', test_file,
                                        'dest_foo', self.data_length)

            checksum = hashlib.sha256()
            test_file.seek(0)
            for _c in range(0, self.num_chunks):
                checksum.update(test_file.read(self.chunk_size))

            # Ensure the files are equal
            self.assertEqual(checksum.digest(), self.checksum.digest())

    @common_mocks
    def test_backup_volume_from_file(self):
        checksum = hashlib.sha256()
        thread_dict = {}

        def mock_write_data(data, offset):
            checksum.update(data)
            thread_dict['thread'] = threading.current_thread()
            test_file.write(data)

        self.service.rbd.Image.return_value.write.side_effect = mock_write_data

        with mock.patch.object(self.service, '_backup_metadata'):
            with mock.patch.object(self.service, '_discard_bytes'):
                with tempfile.NamedTemporaryFile() as test_file:
                    self.service.backup(self.alt_backup, self.volume_file)

                    # Ensure the files are equal
                    self.assertEqual(checksum.digest(), self.checksum.digest())

        self.assertTrue(self.service.rbd.Image.return_value.write.called)
        self.assertNotEqual(threading.current_thread(), thread_dict['thread'])

    @common_mocks
    def test_get_backup_base_name_without_backup_param(self):
        """Test _get_backup_base_name without backup."""
        name = self.service._get_backup_base_name(self.volume_id)
        self.assertEqual("volume-%s.backup.base" % (self.volume_id), name)

    @common_mocks
    def test_get_backup_base_name_w_backup_and_no_parent(self):
        """Test _get_backup_base_name with backup and no parent."""
        name = self.service._get_backup_base_name(self.volume_id,
                                                  self.backup)
        self.assertEqual("volume-%s.backup.%s" %
                         (self.volume_id, self.backup.id), name)

    @common_mocks
    def test_get_backup_base_name_w_backup_and_parent(self):
        """Test _get_backup_base_name with backup and parent."""
        name = self.service._get_backup_base_name(self.volume_id,
                                                  self.alt_backup)
        base_name = json.loads(self.alt_backup.parent.service_metadata)
        self.assertEqual(base_name["base"], name)

    @common_mocks
    @mock.patch('fcntl.fcntl', spec=True)
    def test_backup_volume_from_rbd(self, mock_fnctl):
        """Test full RBD backup generated successfully."""
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        def mock_write_data():
            self.volume_file.seek(0)
            data = self.volume_file.read(self.data_length)
            self.callstack.append('write')
            checksum.update(data)
            test_file.write(data)

        def mock_read_data():
            self.callstack.append('read')
            return self.volume_file.read(self.data_length)

        self._setup_mock_popen(['out', 'err'],
                               p1hook=mock_read_data,
                               p2hook=mock_write_data)

        self.mock_rbd.RBD.list = mock.Mock()
        self.mock_rbd.RBD.list.return_value = [backup_name]

        with mock.patch.object(self.service, '_backup_metadata'):
            with mock.patch.object(self.service, 'get_backup_snaps') as \
                    mock_get_backup_snaps:
                with mock.patch.object(self.service, '_full_backup') as \
                        mock_full_backup:
                    with mock.patch.object(self.service,
                                           '_try_delete_base_image'):
                        with tempfile.NamedTemporaryFile() as test_file:
                            checksum = hashlib.sha256()
                            image = self.service.rbd.Image()
                            meta = linuxrbd.RBDImageMetadata(image,
                                                             'pool_foo',
                                                             'user_foo',
                                                             'conf_foo')
                            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
                            mock_get_backup_snaps.return_value = (
                                [{'name': 'backup.mock.snap.153464362.12'},
                                 {'name': 'backup.mock.snap.15341241.90'},
                                 {'name': 'backup.mock.snap.199994362.10'}])

                            output = self.service.backup(self.alt_backup,
                                                         rbdio)
                            base_name = '{"base": "%s"}' % backup_name
                            service_meta = {'service_metadata': base_name}
                            self.assertDictEqual(service_meta, output)

                            self.assertEqual(['popen_init',
                                              'read',
                                              'popen_init',
                                              'write',
                                              'stdout_close',
                                              'communicate',
                                              'wait'], self.callstack)

                            self.assertFalse(mock_full_backup.called)
                            self.assertFalse(mock_get_backup_snaps.called)

                            # Ensure the files are equal
                            self.assertEqual(checksum.digest(),
                                             self.checksum.digest())

    @common_mocks
    def test_backup_snapshot_lifecycle(self):
        with mock.patch.object(self.service, '_rbd_diff_transfer'), \
            mock.patch.object(self.service, "get_backup_snaps") \
                as mock_get_backup_snaps:

            CONF.set_override('backup_ceph_max_snapshots', 1)

            mocked_snaps = [
                {'name': 'backup.mock.snap.153464362.12'},
                {'name': 'backup.mock.snap.225341241.90'},
                {'name': 'backup.mock.snap.399994362.10'}]

            mock_get_backup_snaps.return_value = mocked_snaps
            self.mock_rbd.RBD.remove_snap = mock.Mock()

            image = self.service.rbd.Image()
            meta = linuxrbd.RBDImageMetadata(image,
                                             'pool_foo',
                                             'user_foo',
                                             'conf_foo')
            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
            rbdio.seek(0)

            self.service._backup_rbd(self.backup, rbdio,
                                     self.volume.name, self.volume.size)

            self.assertEqual(2, self.mock_rbd.Image.return_value.
                             remove_snap.call_count)
            expected_calls = [mock.call('backup.mock.snap.153464362.12'),
                              mock.call('backup.mock.snap.225341241.90')]
            self.mock_rbd.Image.return_value.remove_snap.\
                assert_has_calls(expected_calls)

    @common_mocks
    def test_backup_volume_from_rbd_set_parent_id(self):
        with mock.patch.object(self.service, '_backup_rbd') as \
                mock_backup_rbd, mock.patch.object(self.service,
                                                   '_backup_metadata'):
            mock_backup_rbd.return_value = {'service_metadata': 'base_name'}
            image = self.service.rbd.Image()
            meta = linuxrbd.RBDImageMetadata(image,
                                             'pool_foo',
                                             'user_foo',
                                             'conf_foo')
            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
            output = self.service.backup(self.backup, rbdio)
            self.assertDictEqual({'service_metadata': 'base_name'}, output)

    @common_mocks
    def test_backup_volume_from_rbd_got_exception(self):
        base_name = self.service._get_backup_base_name(self.volume_id,
                                                       self.alt_backup)

        self.mock_rbd.RBD().list.return_value = [base_name]

        with mock.patch.object(self.service, 'get_backup_snaps'), \
                mock.patch.object(self.service, '_rbd_diff_transfer') as \
                mock_rbd_diff_transfer:
            def mock_rbd_diff_transfer_side_effect(src_name, src_pool,
                                                   dest_name, dest_pool,
                                                   src_user, src_conf,
                                                   dest_user, dest_conf,
                                                   src_snap, from_snap):
                raise exception.BackupRBDOperationFailed(_('mock'))

            # Raise a pseudo exception.BackupRBDOperationFailed.
            mock_rbd_diff_transfer.side_effect \
                = mock_rbd_diff_transfer_side_effect

            with mock.patch.object(self.service, '_full_backup'), \
                    mock.patch.object(self.service,
                                      '_try_delete_base_image'):
                with mock.patch.object(self.service, '_backup_metadata'):
                    with mock.patch.object(self.service,
                                           'get_backup_snaps') as \
                            mock_get_backup_snaps:
                        image = self.service.rbd.Image()
                        meta = linuxrbd.RBDImageMetadata(image,
                                                         'pool_foo',
                                                         'user_foo',
                                                         'conf_foo')
                        rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
                        mock_get_backup_snaps.return_value = (
                            [{'name': 'backup.mock.snap.153464362.12',
                              'backup_id': 'mock_parent_id'},
                             {'name': 'backup.mock.snap.199994362.10',
                              'backup_id': 'mock'}])
                        self.assertRaises(exception.BackupRBDOperationFailed,
                                          self.service.backup,
                                          self.alt_backup, rbdio)

    @common_mocks
    def test_backup_rbd_set_parent_id(self):
        base_name = self.service._get_backup_base_name(self.volume_id,
                                                       self.alt_backup)
        vol_name = self.volume.name
        vol_length = self.volume.size

        self.mock_rbd.RBD().list.return_value = [base_name]

        with mock.patch.object(self.service, '_snap_exists'), \
                mock.patch.object(self.service, '_get_backup_snap_name') as \
                mock_get_backup_snap_name, \
                mock.patch.object(self.service, '_rbd_diff_transfer'):
            image = self.service.rbd.Image()
            mock_get_backup_snap_name.return_value = 'mock_snap_name'
            meta = linuxrbd.RBDImageMetadata(image,
                                             'pool_foo',
                                             'user_foo',
                                             'conf_foo')
            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
            rbdio.seek(0)
            output = self.service._backup_rbd(self.alt_backup, rbdio,
                                              vol_name, vol_length)
            base_name = '{"base": "%s"}' % base_name
            self.assertEqual({'service_metadata': base_name}, output)
            self.backup.parent_id = None

    @common_mocks
    def test_backup_rbd_without_parent_id(self):
        full_backup_name = self.service._get_backup_base_name(self.volume_id,
                                                              self.alt_backup)
        vol_name = self.volume.name
        vol_length = self.volume.size

        with mock.patch.object(self.service, '_rbd_diff_transfer'), \
                mock.patch.object(self.service, '_create_base_image') as \
                mock_create_base_image, mock.patch.object(
                rbd_driver, 'RADOSClient') as mock_rados_client:
            client = mock.Mock()
            mock_rados_client.return_value.__enter__.return_value = client
            image = self.service.rbd.Image()
            meta = linuxrbd.RBDImageMetadata(image,
                                             'pool_foo',
                                             'user_foo',
                                             'conf_foo')
            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
            rbdio.seek(0)
            output = self.service._backup_rbd(self.alt_backup, rbdio,
                                              vol_name, vol_length)
            mock_create_base_image.assert_called_with(full_backup_name,
                                                      vol_length, client)
            base_name = '{"base": "%s"}' % full_backup_name
            self.assertEqual({'service_metadata': base_name}, output)

    @common_mocks
    @mock.patch('fcntl.fcntl', spec=True)
    def test_backup_volume_from_rbd_fail(self, mock_fnctl):
        """Test of when an exception occurs in an exception handler.

        In _backup_rbd(), after an exception.BackupRBDOperationFailed
        occurs in self._rbd_diff_transfer(), we want to check the
        process when the second exception occurs in
        self._try_delete_base_image().
        """
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        def mock_write_data():
            self.volume_file.seek(0)
            data = self.volume_file.read(self.data_length)
            self.callstack.append('write')
            checksum.update(data)
            test_file.write(data)

        def mock_read_data():
            self.callstack.append('read')
            return self.volume_file.read(self.data_length)

        self._setup_mock_popen(['out', 'err'],
                               p1hook=mock_read_data,
                               p2hook=mock_write_data)

        self.mock_rbd.RBD.list = mock.Mock()
        self.mock_rbd.RBD.list.return_value = [backup_name]

        with mock.patch.object(self.service, 'get_backup_snaps') as \
                mock_get_backup_snaps:
            mock_get_backup_snaps.return_value = (
                [{'name': 'backup.mock.snap.153464362.12'},
                 {'name': 'backup.mock.snap.199994362.10'}])
            with mock.patch.object(self.service, '_rbd_diff_transfer') as \
                    mock_rbd_diff_transfer:
                def mock_rbd_diff_transfer_side_effect(src_name, src_pool,
                                                       dest_name, dest_pool,
                                                       src_user, src_conf,
                                                       dest_user, dest_conf,
                                                       src_snap, from_snap):
                    raise exception.BackupRBDOperationFailed(_('mock'))

                # Raise a pseudo exception.BackupRBDOperationFailed.
                mock_rbd_diff_transfer.side_effect \
                    = mock_rbd_diff_transfer_side_effect

                with mock.patch.object(self.service, '_full_backup'), \
                    mock.patch.object(self.service,
                                      '_try_delete_base_image') as \
                        mock_try_delete_base_image:
                    def mock_try_delete_base_image_side_effect(backup_id,
                                                               base_name):
                        raise self.service.rbd.ImageNotFound(_('mock'))

                    # Raise a pesudo exception rbd.ImageNotFound.
                    mock_try_delete_base_image.side_effect \
                        = mock_try_delete_base_image_side_effect
                    with mock.patch.object(self.service, '_backup_metadata'):
                        with tempfile.NamedTemporaryFile() as test_file:
                            checksum = hashlib.sha256()
                            image = self.service.rbd.Image()
                            meta = linuxrbd.RBDImageMetadata(image,
                                                             'pool_foo',
                                                             'user_foo',
                                                             'conf_foo')
                            rbdio = linuxrbd.RBDVolumeIOWrapper(meta)

                            # We expect that the second exception is
                            # notified.
                            self.assertRaises(
                                self.service.rbd.ImageNotFound,
                                self.service.backup,
                                self.alt_backup, rbdio)

    @common_mocks
    @mock.patch('fcntl.fcntl', spec=True)
    def test_backup_volume_from_rbd_fail2(self, mock_fnctl):
        """Test of when an exception occurs in an exception handler.

        In backup(), after an exception.BackupOperationError occurs in
        self._backup_metadata(), we want to check the process when the
        second exception occurs in self.delete_backup().
        """
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        def mock_write_data():
            self.volume_file.seek(0)
            data = self.volume_file.read(self.data_length)
            self.callstack.append('write')
            checksum.update(data)
            test_file.write(data)

        def mock_read_data():
            self.callstack.append('read')
            return self.volume_file.read(self.data_length)

        self._setup_mock_popen(['out', 'err'],
                               p1hook=mock_read_data,
                               p2hook=mock_write_data)

        self.mock_rbd.RBD.list = mock.Mock()
        self.mock_rbd.RBD.list.return_value = [backup_name]

        with mock.patch.object(self.service, 'get_backup_snaps') as \
                mock_get_backup_snaps:
            mock_get_backup_snaps.return_value = (
                [{'name': 'backup.mock.snap.153464362.12'},
                 {'name': 'backup.mock.snap.199994362.10'}])
            with mock.patch.object(self.service, '_rbd_diff_transfer'), \
                mock.patch.object(self.service, '_full_backup'), \
                mock.patch.object(self.service, '_backup_metadata') as \
                    mock_backup_metadata:

                def mock_backup_metadata_side_effect(backup):
                    raise exception.BackupOperationError(_('mock'))

                # Raise a pseudo exception.BackupOperationError.
                mock_backup_metadata.side_effect = (
                    mock_backup_metadata_side_effect)
                with mock.patch.object(self.service, 'delete_backup') as \
                        mock_delete:
                    def mock_delete_side_effect(backup):
                        raise self.service.rbd.ImageBusy()

                    # Raise a pseudo exception rbd.ImageBusy.
                    mock_delete.side_effect = mock_delete_side_effect
                    with tempfile.NamedTemporaryFile() as test_file:
                        checksum = hashlib.sha256()
                        image = self.service.rbd.Image()
                        meta = linuxrbd.RBDImageMetadata(image,
                                                         'pool_foo',
                                                         'user_foo',
                                                         'conf_foo')
                        rbdio = linuxrbd.RBDVolumeIOWrapper(meta)

                        # We expect that the second exception is
                        # notified.
                        self.assertRaises(
                            self.service.rbd.ImageBusy,
                            self.service.backup,
                            self.alt_backup, rbdio)

    @common_mocks
    def test_backup_rbd_from_snap(self):
        backup_name = self.service._get_backup_base_name(self.volume_id)
        vol_name = self.volume['name']
        vol_length = self.service._get_volume_size_bytes(self.volume)

        self.mock_rbd.RBD().list = mock.Mock()
        self.mock_rbd.RBD().list.return_value = ['mock']

        with mock.patch.object(self.service, '_get_new_snap_name') as \
                mock_get_new_snap_name:
            with mock.patch.object(self.service, 'get_backup_snaps') as \
                    mock_get_backup_snaps:
                with mock.patch.object(self.service, '_rbd_diff_transfer') as \
                        mock_rbd_diff_transfer:
                    with mock.patch.object(self.service,
                                           '_get_backup_base_name') as \
                            mock_get_backup_base_name:
                        mock_get_backup_base_name.return_value = (
                            backup_name)
                        mock_get_backup_snaps.return_value = (
                            [{'name': 'backup.mock.snap.153464362.12'},
                             {'name': 'backup.mock.snap.15341241.90'},
                             {'name': 'backup.mock.snap.199994362.10'}])
                        mock_get_new_snap_name.return_value = 'new_snap'
                        image = self.service.rbd.Image()
                        meta = linuxrbd.RBDImageMetadata(image,
                                                         'pool_foo',
                                                         'user_foo',
                                                         'conf_foo')
                        rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
                        rbdio.seek(0)
                        self.service._backup_rbd(self.backup, rbdio,
                                                 vol_name, vol_length)
                        mock_rbd_diff_transfer.assert_called_with(
                            vol_name, 'pool_foo', backup_name,
                            self.backup.container, src_user='user_foo',
                            src_conf='conf_foo',
                            dest_conf='/etc/ceph/ceph.conf',
                            dest_user='cinder', src_snap='new_snap',
                            from_snap=None)

    @common_mocks
    def test_backup_rbd_from_snap2(self):
        base_name = self.service._get_backup_base_name(self.volume_id,
                                                       self.alt_backup)
        vol_name = self.volume['name']
        vol_length = self.service._get_volume_size_bytes(self.volume)

        self.mock_rbd.RBD().list = mock.Mock()
        self.mock_rbd.RBD().list.return_value = [base_name]

        with mock.patch.object(self.service, '_get_backup_base_name') as \
                mock_get_backup_base_name:
            with mock.patch.object(self.service, '_rbd_diff_transfer') as \
                    mock_rbd_diff_transfer:
                with mock.patch.object(self.service, '_get_new_snap_name') as \
                        mock_get_new_snap_name:
                    mock_get_backup_base_name.return_value = base_name
                    mock_get_new_snap_name.return_value = 'new_snap'
                    image = self.service.rbd.Image()
                    meta = linuxrbd.RBDImageMetadata(image, 'pool_foo',
                                                     'user_foo', 'conf_foo')
                    rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
                    rbdio.seek(0)
                    self.service._backup_rbd(self.alt_backup, rbdio, vol_name,
                                             vol_length)
                    mock_rbd_diff_transfer.assert_called_with(
                        vol_name, 'pool_foo', base_name,
                        self.backup.container, src_user='user_foo',
                        src_conf='conf_foo',
                        dest_conf='/etc/ceph/ceph.conf',
                        dest_user='cinder', src_snap='new_snap',
                        from_snap=None)

    @common_mocks
    def test_backup_vol_length_0(self):
        volume_id = fake.VOLUME4_ID
        self._create_volume_db_entry(volume_id, 0)
        backup_id = fake.BACKUP4_ID
        self._create_backup_db_entry(backup_id, volume_id, 1)
        backup = objects.Backup.get_by_id(self.ctxt, backup_id)

        self.assertRaises(exception.InvalidParameterValue, self.service.backup,
                          backup, self.volume_file)

    @common_mocks
    def test_backup_with_container_name(self):
        volume_size = self.volume_size * units.Gi
        backup_id = fake.BACKUP4_ID
        self._create_backup_db_entry(backup_id, self.volume_id, 1)
        backup = objects.Backup.get_by_id(self.ctxt, backup_id)
        backup.container = "test"
        with mock.patch.object(
                self.service, '_full_backup',
                side_effect=exception.BackupOperationError()) as mock_full:
            self.assertRaises(exception.BackupOperationError,
                              self.service.backup, backup, self.volume_file)
            mock_full.assert_called_once_with(backup, self.volume_file,
                                              self.volume.name, volume_size)

    @common_mocks
    def test_restore(self):
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        self.mock_rbd.RBD.return_value.list.return_value = [backup_name]

        thread_dict = {}

        def mock_read_data(offset, length):
            thread_dict['thread'] = threading.current_thread()
            return self.volume_file.read(self.data_length)

        self.mock_rbd.Image.return_value.read.side_effect = mock_read_data

        self.mock_rbd.Image.return_value.size.return_value = \
            self.chunk_size * self.num_chunks

        with mock.patch.object(self.service, '_restore_metadata') as \
                mock_restore_metadata:
            with mock.patch.object(self.service, '_discard_bytes') as \
                    mock_discard_bytes:
                with tempfile.NamedTemporaryFile() as test_file:
                    self.volume_file.seek(0)

                    self.service.restore(self.alt_backup, self.volume_id,
                                         test_file, False)

                    checksum = hashlib.sha256()
                    test_file.seek(0)
                    for _c in range(0, self.num_chunks):
                        checksum.update(test_file.read(self.chunk_size))

                    # Ensure the files are equal
                    self.assertEqual(checksum.digest(), self.checksum.digest())

                    self.assertTrue(mock_restore_metadata.called)
                    self.assertTrue(mock_discard_bytes.called)
                    self.assertTrue(mock_discard_bytes.called)

        self.assertTrue(self.service.rbd.Image.return_value.read.called)
        self.assertNotEqual(threading.current_thread(), thread_dict['thread'])

    @common_mocks
    def test_full_restore_without_snapshot_id_nor_src_snap(self):
        length = 1024
        volume_is_new = True
        src_snap = ''
        with tempfile.NamedTemporaryFile() as dest_file:
            with mock.patch.object(self.service,
                                   '_transfer_data') as mock_transfer_data, \
                mock.patch.object(self.service,
                                  '_get_backup_base_name') as mock_getbasename:

                self.service._full_restore(self.backup, dest_file, length,
                                           volume_is_new, src_snap)

                mock_getbasename.assert_called_once_with(self.volume_id,
                                                         backup=self.backup)
                mock_transfer_data.assert_called_once()

    @common_mocks
    def test_full_restore_without_snapshot_id_w_src_snap(self):
        length = 1024
        volume_is_new = True
        src_snap = 'random_snap'
        with tempfile.NamedTemporaryFile() as dest_file:
            with mock.patch.object(self.service,
                                   '_transfer_data') as mock_transfer_data, \
                mock.patch.object(self.service,
                                  '_get_backup_base_name') as mock_getbasename:

                self.service._full_restore(self.backup, dest_file, length,
                                           volume_is_new, src_snap)

                mock_getbasename.assert_called_once_with(self.volume_id,
                                                         backup=self.backup)
                mock_transfer_data.assert_called_once()

    @common_mocks
    def test_full_restore_with_snapshot_id(self):
        length = 1024
        volume_is_new = True
        src_snap = ''

        # Create alternate backup with snapshot_id
        backup_id = fake.BACKUP4_ID
        self._create_backup_db_entry(backup_id, self.volume_id,
                                     self.volume_size)

        backup = objects.Backup.get_by_id(self.ctxt, backup_id)
        backup.snapshot_id = 'random_snap_id'
        backup.container = "backups"
        backup.parent = self.backup
        backup.parent.service_metadata = '{"base": "random"}'

        with tempfile.NamedTemporaryFile() as dest_file:
            with mock.patch.object(self.service,
                                   '_transfer_data') as mock_transfer_data, \
                mock.patch.object(self.service,
                                  '_get_backup_base_name') as mock_getbasename:

                self.service._full_restore(backup, dest_file, length,
                                           volume_is_new, src_snap)

                mock_getbasename.assert_called_once_with(self.volume_id)
                mock_transfer_data.assert_called_once()

    @common_mocks
    def test_full_restore_with_image_not_found(self):
        length = 1024
        volume_is_new = True
        src_snap = None
        with tempfile.NamedTemporaryFile() as dest_file:
            with mock.patch.object(self.service,
                                   '_get_backup_base_name') as mock_name, \
                    mock.patch('eventlet.tpool.Proxy') as mock_proxy:

                self.mock_rbd.Image.side_effect = self.mock_rbd.ImageNotFound

                self.assertRaises(self.mock_rbd.ImageNotFound,
                                  self.service._full_restore,
                                  self.backup,
                                  dest_file,
                                  length,
                                  volume_is_new,
                                  src_snap)

                # Check that the _get_backup_base_name was called
                # twice due to the exception
                self.assertEqual(mock_name.call_count, 2)
                self.assertEqual(mock_proxy.call_count, 2)

    @common_mocks
    def test_discard_bytes(self):
        # Lower the chunksize to a memory manageable number
        thread_dict = {}
        self.service.chunk_size = 1024
        image = self.mock_rbd.Image.return_value
        wrapped_rbd = self._get_wrapped_rbd_io(image)

        def mock_discard(offset, length):
            thread_dict['thread'] = threading.current_thread()
            return self.mock_rbd.Image.discard(offset, length)

        self.mock_rbd.Image.return_value.discard.side_effect = mock_discard

        self.service._discard_bytes(wrapped_rbd, 0, 0)
        self.assertEqual(0, image.discard.call_count)
        image.discard.reset_mock()

        self.service._discard_bytes(wrapped_rbd, 0, 1234)
        self.assertEqual(1, image.discard.call_count)
        image.discard.assert_has_calls([mock.call(0, 1234)])
        image.discard.reset_mock()

        limit = 2 * units.Gi - 1
        self.service._discard_bytes(wrapped_rbd, 0, limit)
        self.assertEqual(1, image.discard.call_count)
        image.discard.assert_has_calls([mock.call(0, 2147483647)])
        image.discard.reset_mock()

        self.service._discard_bytes(wrapped_rbd, 0, limit * 2 + 1234)
        self.assertEqual(3, image.discard.call_count)
        image.discard.assert_has_calls([mock.call(0, 2147483647),
                                        mock.call(2147483647, 2147483647),
                                        mock.call(4294967294, 1234)])
        image.reset_mock()

        # Test discard with no remainder
        with mock.patch.object(self.service, '_file_is_rbd') as \
                mock_file_is_rbd:
            mock_file_is_rbd.return_value = False

            self.service._discard_bytes(wrapped_rbd, 0,
                                        self.service.chunk_size * 2)

            self.assertEqual(2, image.write.call_count)
            self.assertEqual(2, image.flush.call_count)
            self.assertFalse(image.discard.called)
            zeroes = bytearray(self.service.chunk_size)
            image.write.assert_has_calls([mock.call(zeroes, 0),
                                         mock.call(zeroes, self.chunk_size)])
            self.assertNotEqual(threading.current_thread(),
                                thread_dict['thread'])

        image.reset_mock()
        image.write.reset_mock()

        # Now test with a remainder.
        with mock.patch.object(self.service, '_file_is_rbd') as \
                mock_file_is_rbd:
            mock_file_is_rbd.return_value = False

            self.service._discard_bytes(wrapped_rbd, 0,
                                        (self.service.chunk_size * 2) + 1)

            self.assertEqual(3, image.write.call_count)
            self.assertEqual(3, image.flush.call_count)
            self.assertFalse(image.discard.called)
            image.write.assert_has_calls([mock.call(zeroes,
                                                    self.chunk_size * 2),
                                          mock.call(zeroes,
                                                    self.chunk_size * 3),
                                          mock.call(bytearray(1),
                                                    self.chunk_size * 4)])

    @common_mocks
    def test_delete_backup_snapshot(self):
        snap_name = 'backup.%s.snap.3824923.1412' % fake.UUID1
        base_name = self.service._get_backup_base_name(self.volume_id)
        self.mock_rbd.RBD.remove_snap = mock.Mock()
        thread_dict = {}

        def mock_side_effect(snap):
            thread_dict['thread'] = threading.current_thread()

        self.mock_rbd.Image.return_value.remove_snap.side_effect = \
            mock_side_effect

        with mock.patch.object(self.service, '_get_backup_snap_name') as \
                mock_get_backup_snap_name:
            mock_get_backup_snap_name.return_value = snap_name
            with mock.patch.object(self.service, 'get_backup_snaps') as \
                    mock_get_backup_snaps:
                mock_get_backup_snaps.return_value = None
                rem = self.service._delete_backup_snapshot(self.mock_rados,
                                                           base_name,
                                                           self.backup_id)

                self.assertTrue(mock_get_backup_snap_name.called)
                self.assertTrue(mock_get_backup_snaps.called)
                self.assertEqual((snap_name, 0), rem)
                self.assertNotEqual(threading.current_thread(),
                                    thread_dict['thread'])

    @common_mocks
    @mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
    def test_try_delete_base_image_diff_format(self, mock_meta_backup):
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        self.mock_rbd.RBD.return_value.list.return_value = [backup_name]

        with mock.patch.object(self.service, '_delete_backup_snapshot') as \
                mock_del_backup_snap:
            snap_name = self.service._get_new_snap_name(self.alt_backup_id)
            mock_del_backup_snap.return_value = (snap_name, 0)

            self.service.delete_backup(self.alt_backup)
            self.assertTrue(mock_del_backup_snap.called)

        self.assertTrue(self.mock_rbd.RBD.return_value.list.called)
        self.assertTrue(self.mock_rbd.RBD.return_value.remove.called)

    @common_mocks
    @mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
    def test_try_delete_base_image(self, mock_meta_backup):
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)
        thread_dict = {}

        def mock_side_effect(ioctx, base_name):
            thread_dict['thread'] = threading.current_thread()

        self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
        self.mock_rbd.RBD.return_value.remove.side_effect = mock_side_effect
        with mock.patch.object(self.service, 'get_backup_snaps'):
            self.service.delete_backup(self.alt_backup)
            self.assertTrue(self.mock_rbd.RBD.return_value.remove.called)
            self.assertNotEqual(threading.current_thread(),
                                thread_dict['thread'])

    @common_mocks
    def test_try_delete_base_image_busy(self):
        """This should induce retries then raise rbd.ImageBusy."""
        backup_name = self.service._get_backup_base_name(self.volume_id,
                                                         self.alt_backup)

        rbd = self.mock_rbd.RBD.return_value
        rbd.list.return_value = [backup_name]
        rbd.remove.side_effect = self.mock_rbd.ImageBusy

        with mock.patch.object(self.service, 'get_backup_snaps') as \
                mock_get_backup_snaps:
            self.assertRaises(self.mock_rbd.ImageBusy,
                              self.service._try_delete_base_image,
                              self.alt_backup)
            self.assertTrue(mock_get_backup_snaps.called)

        self.assertTrue(rbd.list.called)
        self.assertTrue(rbd.remove.called)
        self.assertIn(MockImageBusyException, RAISED_EXCEPTIONS)

    @common_mocks
    @mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
    def test_delete_image_not_found(self, mock_meta_backup):
        with mock.patch.object(self.service, '_try_delete_base_image') as \
                mock_del_base:
            mock_del_base.side_effect = self.mock_rbd.ImageNotFound
            # ImageNotFound exception is caught so that db entry can be cleared
            self.service.delete_backup(self.backup)
            self.assertEqual([MockImageNotFoundException], RAISED_EXCEPTIONS)

    @common_mocks
    @mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
    def test_delete_pool_not_found(self, mock_meta_backup):
        with mock.patch.object(
                self.service, '_try_delete_base_image') as mock_del_base:
            mock_del_base.side_effect = self.mock_rados.ObjectNotFound
            # ObjectNotFound exception is caught so that db entry can be
            # cleared
            self.service.delete_backup(self.backup)
            self.assertEqual([MockObjectNotFoundException],
                             RAISED_EXCEPTIONS)
            mock_del_base.assert_called_once_with(self.backup)
            mock_meta_backup.return_value.remove_if_exists.assert_not_called()

    @common_mocks
    def test_diff_restore_allowed_with_image_not_exists(self):
        """Test diff restore not allowed when backup not diff-format."""
        not_allowed = (False, None)
        backup_base = 'backup.base'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_different = [backup_base, self.backup, self.alt_volume,
                               rbd_io, self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (False, backup_base)

            resp = self.service._diff_restore_allowed(*args_vols_different)

            self.assertEqual(not_allowed, resp)
            mock_rbd_image_exists.assert_called_once_with(
                backup_base,
                self.backup['volume_id'],
                self.mock_rados)

    @common_mocks
    def test_diff_restore_allowed_with_no_restore_point(self):
        """Test diff restore not allowed when no restore point found.

        Detail conditions:
          1. backup base is diff-format
          2. restore point does not exist
        """
        not_allowed = (False, None)
        backup_base = 'backup.base'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_different = [backup_base, self.backup, self.alt_volume,
                               rbd_io, self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (True, backup_base)

            with mock.patch.object(self.service, '_get_restore_point') as \
                    mock_get_restore_point:
                mock_get_restore_point.return_value = None

                args = args_vols_different
                resp = self.service._diff_restore_allowed(*args)

                self.assertEqual(not_allowed, resp)
                self.assertTrue(mock_rbd_image_exists.called)
                mock_get_restore_point.assert_called_once_with(
                    backup_base,
                    self.backup['id'])

    @common_mocks
    def test_diff_restore_allowed_with_not_rbd(self):
        """Test diff restore not allowed when destination volume is not rbd.

        Detail conditions:
          1. backup base is diff-format
          2. restore point exists
          3. destination volume is not an rbd.
        """
        backup_base = 'backup.base'
        restore_point = 'backup.snap.1'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_different = [backup_base, self.backup, self.alt_volume,
                               rbd_io, self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (True, backup_base)
            with mock.patch.object(self.service, '_get_restore_point') as \
                    mock_get_restore_point:
                mock_get_restore_point.return_value = restore_point
                with mock.patch.object(self.service, '_file_is_rbd') as \
                        mock_file_is_rbd:
                    mock_file_is_rbd.return_value = False

                    args = args_vols_different
                    resp = self.service._diff_restore_allowed(*args)

                    self.assertEqual((False, restore_point), resp)
                    self.assertTrue(mock_rbd_image_exists.called)
                    self.assertTrue(mock_get_restore_point.called)
                    mock_file_is_rbd.assert_called_once_with(
                        rbd_io)

    @common_mocks
    def test_diff_restore_allowed_with_same_volume(self):
        """Test diff restore not allowed when volumes are same.

        Detail conditions:
          1. backup base is diff-format
          2. restore point exists
          3. destination volume is an rbd
          4. source and destination volumes are the same
        """
        backup_base = 'backup.base'
        restore_point = 'backup.snap.1'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_same = [backup_base, self.backup, self.volume, rbd_io,
                          self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (True, backup_base)
            with mock.patch.object(self.service, '_get_restore_point') as \
                    mock_get_restore_point:
                mock_get_restore_point.return_value = restore_point
                with mock.patch.object(self.service, '_file_is_rbd') as \
                        mock_file_is_rbd:
                    mock_file_is_rbd.return_value = True

                    resp = self.service._diff_restore_allowed(*args_vols_same)

                    self.assertEqual((False, restore_point), resp)
                    self.assertTrue(mock_rbd_image_exists.called)
                    self.assertTrue(mock_get_restore_point.called)
                    self.assertTrue(mock_file_is_rbd.called)

    @common_mocks
    def test_diff_restore_allowed_with_has_extents(self):
        """Test diff restore not allowed when destination volume has data.

        Detail conditions:
          1. backup base is diff-format
          2. restore point exists
          3. destination volume is an rbd
          4. source and destination volumes are different
          5. destination volume has data on it - full copy is mandated
        """
        backup_base = 'backup.base'
        restore_point = 'backup.snap.1'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_different = [backup_base, self.backup, self.alt_volume,
                               rbd_io, self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (True, backup_base)
            with mock.patch.object(self.service, '_get_restore_point') as \
                    mock_get_restore_point:
                mock_get_restore_point.return_value = restore_point
                with mock.patch.object(self.service, '_file_is_rbd') as \
                        mock_file_is_rbd:
                    mock_file_is_rbd.return_value = True
                    with mock.patch.object(self.service, '_rbd_has_extents') \
                            as mock_rbd_has_extents:
                        mock_rbd_has_extents.return_value = True

                        args = args_vols_different
                        resp = self.service._diff_restore_allowed(*args)

                        self.assertEqual((False, restore_point), resp)
                        self.assertTrue(mock_rbd_image_exists.called)
                        self.assertTrue(mock_get_restore_point.called)
                        self.assertTrue(mock_file_is_rbd.called)
                        mock_rbd_has_extents.assert_called_once_with(
                            rbd_io.rbd_image)

    @common_mocks
    def test_diff_restore_allowed_with_no_extents(self):
        """Test diff restore allowed when no data in destination volume.

        Detail conditions:
          1. backup base is diff-format
          2. restore point exists
          3. destination volume is an rbd
          4. source and destination volumes are different
          5. destination volume no data on it
        """
        backup_base = 'backup.base'
        restore_point = 'backup.snap.1'
        rbd_io = self._get_wrapped_rbd_io(self.service.rbd.Image())
        args_vols_different = [backup_base, self.backup, self.alt_volume,
                               rbd_io, self.mock_rados]

        with mock.patch.object(self.service, '_rbd_image_exists') as \
                mock_rbd_image_exists:
            mock_rbd_image_exists.return_value = (True, backup_base)
            with mock.patch.object(self.service, '_get_restore_point') as \
                    mock_get_restore_point:
                mock_get_restore_point.return_value = restore_point
                with mock.patch.object(self.service, '_file_is_rbd') as \
                        mock_file_is_rbd:
                    mock_file_is_rbd.return_value = True
                    with mock.patch.object(self.service, '_rbd_has_extents') \
                            as mock_rbd_has_extents:
                        mock_rbd_has_extents.return_value = False

                        args = args_vols_different
                        resp = self.service._diff_restore_allowed(*args)

                        self.assertEqual((True, restore_point), resp)
                        self.assertTrue(mock_rbd_image_exists.called)
                        self.assertTrue(mock_get_restore_point.called)
                        self.assertTrue(mock_file_is_rbd.called)
                        self.assertTrue(mock_rbd_has_extents.called)

    @common_mocks
    @mock.patch('fcntl.fcntl', spec=True)
    def test_piped_execute(self, mock_fcntl):
        mock_fcntl.return_value = 0
        self._setup_mock_popen(['out', 'err'])
        self.service._piped_execute(['foo'], ['bar'])
        self.assertEqual(['popen_init', 'popen_init', 'stdout_close',
                          'communicate', 'wait'], self.callstack)

    @common_mocks
    def test_restore_metdata(self):
        version = 2

        def mock_read(*args):
            base_tag = driver.BackupMetadataAPI.TYPE_TAG_VOL_BASE_META
            glance_tag = driver.BackupMetadataAPI.TYPE_TAG_VOL_GLANCE_META
            return jsonutils.dumps({base_tag: {'image_name': 'image.base'},
                                    glance_tag: {'image_name': 'image.glance'},
                                    'version': version}).encode('utf-8')

        self.mock_rados.Object.return_value.read.side_effect = mock_read

        self.service._restore_metadata(self.backup, self.volume_id)

        self.assertTrue(self.mock_rados.Object.return_value.stat.called)
        self.assertTrue(self.mock_rados.Object.return_value.read.called)

        version = 3
        try:
            self.service._restore_metadata(self.backup, self.volume_id)
        except exception.BackupOperationError as exc:
            msg = _("Metadata restore failed due to incompatible version")
            self.assertEqual(msg, str(exc))
        else:
            # Force a test failure
            self.assertFalse(True)

    @common_mocks
    @mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
    def test_backup_metadata_already_exists(self, mock_meta_backup):

        def mock_set(json_meta):
            msg = (_("Metadata backup object '%s' already exists") %
                   ("backup.%s.meta" % (self.backup_id)))
            raise exception.VolumeMetadataBackupExists(msg)

        mock_meta_backup.return_value.set = mock.Mock()
        mock_meta_backup.return_value.set.side_effect = mock_set

        with mock.patch.object(self.service, 'get_metadata') as \
                mock_get_metadata:
            mock_get_metadata.return_value = "some.json.metadata"
            try:
                self.service._backup_metadata(self.backup)
            except exception.BackupOperationError as e:
                msg = (_("Failed to backup volume metadata - Metadata backup "
                         "object 'backup.%s.meta' already exists") %
                       (self.backup_id))
                self.assertEqual(msg, str(e))
            else:
                # Make the test fail
                self.assertFalse(True)

        self.assertFalse(mock_meta_backup.set.called)

    @common_mocks
    def test_backup_metadata_error(self):
        """Ensure that delete_backup() is called if the metadata backup fails.

        Also ensure that the exception is propagated to the caller.
        """
        with mock.patch.object(self.service, '_backup_metadata') as \
                mock_backup_metadata:
            mock_backup_metadata.side_effect = exception.BackupOperationError
            with mock.patch.object(self.service, '_get_volume_size_bytes'):
                with mock.patch.object(self.service, '_file_is_rbd',
                                       return_value=False):
                    with mock.patch.object(self.service, '_full_backup'):
                        with mock.patch.object(self.service,
                                               'delete_backup') as \
                                mock_delete:
                            self.assertRaises(exception.BackupOperationError,
                                              self.service.backup, self.backup,
                                              mock.Mock(),
                                              backup_metadata=True)
                            self.assertTrue(mock_delete.called)

    @common_mocks
    def test_restore_invalid_metadata_version(self):

        def mock_read(*args):
            base_tag = driver.BackupMetadataAPI.TYPE_TAG_VOL_BASE_META
            glance_tag = driver.BackupMetadataAPI.TYPE_TAG_VOL_GLANCE_META
            return jsonutils.dumps({base_tag: {'image_name': 'image.base'},
                                    glance_tag: {'image_name': 'image.glance'},
                                    'version': 3}).encode('utf-8')

        self.mock_rados.Object.return_value.read.side_effect = mock_read
        with mock.patch.object(ceph.VolumeMetadataBackup, '_exists') as \
                mock_exists:
            mock_exists.return_value = True

            self.assertRaises(exception.BackupOperationError,
                              self.service._restore_metadata,
                              self.backup, self.volume_id)

            self.assertTrue(mock_exists.called)

        self.assertTrue(self.mock_rados.Object.return_value.read.called)

    @ddt.data((None, False),
              ([{'name': 'test'}], False),
              ([{'name': 'test'}, {'name': 'fake'}], True))
    @ddt.unpack
    @common_mocks
    def test__snap_exists(self, snapshots, snap_exist):
        client = mock.Mock()
        thread_dict = {}

        with mock.patch.object(self.service.rbd.Image(),
                               'list_snaps') as snaps:
            snaps.return_value = snapshots

            def mock_side_effect():
                thread_dict['thread'] = threading.current_thread()
                return snaps.return_value

            snaps.side_effect = mock_side_effect
            exist = self.service._snap_exists(None, 'fake', client)
            self.assertEqual(snap_exist, exist)
            self.assertNotEqual(thread_dict['thread'],
                                threading.current_thread())


def common_meta_backup_mocks(f):
    """Decorator to set mocks common to all metadata backup tests.

    The point of doing these mocks here is so that we don't accidentally set
    mocks that can't/don't get unset.
    """
    def _common_inner_inner1(inst, *args, **kwargs):
        @mock.patch('cinder.backup.drivers.ceph.rbd')
        @mock.patch('cinder.backup.drivers.ceph.rados')
        def _common_inner_inner2(mock_rados, mock_rbd):
            inst.mock_rados = mock_rados
            inst.mock_rbd = mock_rbd
            inst.mock_rados.ObjectNotFound = MockObjectNotFoundException
            return f(inst, *args, **kwargs)

        return _common_inner_inner2()
    return _common_inner_inner1


class VolumeMetadataBackupTestCase(test.TestCase):

    def setUp(self):
        global RAISED_EXCEPTIONS
        RAISED_EXCEPTIONS = []
        super(VolumeMetadataBackupTestCase, self).setUp()
        self.backup_id = fake.BACKUP_ID
        self.mb = ceph.VolumeMetadataBackup(mock.Mock(), self.backup_id)

    @common_meta_backup_mocks
    def test_name(self):
        self.assertEqual('backup.%s.meta' % (self.backup_id), self.mb.name)

    @common_meta_backup_mocks
    def test_exists(self):
        thread_dict = {}

        def mock_side_effect():
            thread_dict['thread'] = threading.current_thread()

        # True
        self.mock_rados.Object.return_value.stat.side_effect = mock_side_effect
        self.assertTrue(self.mb.exists)
        self.assertTrue(self.mock_rados.Object.return_value.stat.called)
        self.mock_rados.Object.return_value.reset_mock()
        self.assertNotEqual(thread_dict['thread'], threading.current_thread())

        # False
        self.mock_rados.Object.return_value.stat.side_effect = (
            self.mock_rados.ObjectNotFound)
        self.assertFalse(self.mb.exists)
        self.assertTrue(self.mock_rados.Object.return_value.stat.called)
        self.assertEqual([MockObjectNotFoundException], RAISED_EXCEPTIONS)

    @common_meta_backup_mocks
    def test_set(self):
        obj_data = []
        called = []
        thread_dict = {}

        def mock_read(*args):
            called.append('read')
            self.assertEqual(1, len(obj_data))
            return obj_data[0]

        def _mock_write(data):
            obj_data.append(data)
            called.append('write')
            thread_dict['thread'] = threading.current_thread()

        self.mb.get = mock.Mock()
        self.mb.get.side_effect = mock_read
        serialized_meta_1 = jsonutils.dumps({'foo': 'bar'})
        serialized_meta_2 = jsonutils.dumps({'doo': 'dah'})

        with mock.patch.object(ceph.VolumeMetadataBackup, 'set') as mock_write:
            mock_write.side_effect = _mock_write

            self.mb.set(serialized_meta_1)
            self.assertEqual(serialized_meta_1, self.mb.get())
            self.assertTrue(self.mb.get.called)

            self.mb._exists = mock.Mock()
            self.mb._exists.return_value = True

        # use the unmocked set() method.
        self.assertRaises(exception.VolumeMetadataBackupExists,
                          self.mb.set, serialized_meta_2)

        # check the meta obj state has not changed.
        self.assertEqual(serialized_meta_1, self.mb.get())

        self.assertEqual(['write', 'read', 'read'], called)

        self.mb._exists.return_value = False
        self.mb.set(serialized_meta_2)
        self.assertNotEqual(thread_dict['thread'],
                            threading.current_thread)

    @common_meta_backup_mocks
    def test_get(self):
        self.mock_rados.Object.return_value.stat.side_effect = (
            self.mock_rados.ObjectNotFound)
        self.mock_rados.Object.return_value.read.return_value = (
            'meta'.encode('utf-8'))
        self.assertIsNone(self.mb.get())
        self.mock_rados.Object.return_value.stat.side_effect = None
        self.assertEqual('meta', self.mb.get())

    @common_meta_backup_mocks
    def remove_if_exists(self):
        thread_dict = {}

        def mock_side_effect():
            thread_dict['thread'] = threading.current_thread()

        with mock.patch.object(self.mock_rados.Object, 'remove') as \
                mock_remove:
            mock_remove.side_effect = self.mock_rados.ObjectNotFound
            self.mb.remove_if_exists()
            self.assertEqual([MockObjectNotFoundException], RAISED_EXCEPTIONS)

            self.mock_rados.Object.remove.side_effect = mock_side_effect
            self.mb.remove_if_exists()
            self.assertEqual([], RAISED_EXCEPTIONS)
            self.assertNotEqual(thread_dict['thread'],
                                threading.current_thread)
