
Change I743e676372703e74178c79683dd622d530981e04 removed volume backend driver calls for creating and restoring volume backups. The overridden methods for creating and restoring backups in the VMDK driver is no longer called and this breaks the backup- restore of volumes created by the VMDK driver. The Cinder backup manager now uses initiator connectors for creating and restoring backups for all volume backends. The patch adds a connector for vmdk volumes to fix the backup-restore for the VMDK driver. DocImpact Change-Id: Ia1a20f93780593b1efbb74484c3fdd3ca3564290 Partial-bug: #1602660
367 lines
14 KiB
Python
367 lines
14 KiB
Python
# Copyright (c) 2016 VMware, Inc.
|
|
# 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.
|
|
|
|
import ddt
|
|
import mock
|
|
from oslo_utils import units
|
|
from oslo_vmware.objects import datastore
|
|
from oslo_vmware import vim_util
|
|
|
|
from os_brick import exception
|
|
from os_brick.initiator.connectors import vmware
|
|
from os_brick.tests.initiator import test_connector
|
|
|
|
|
|
@ddt.ddt
|
|
class VmdkConnectorTestCase(test_connector.ConnectorTestCase):
|
|
|
|
IP = '127.0.0.1'
|
|
PORT = 443
|
|
USERNAME = 'username'
|
|
PASSWORD = 'password'
|
|
API_RETRY_COUNT = 3
|
|
TASK_POLL_INTERVAL = 5.0
|
|
CA_FILE = "/etc/ssl/rui-ca-cert.pem"
|
|
TMP_DIR = "/vmware-tmp"
|
|
IMG_TX_TIMEOUT = 10
|
|
|
|
VMDK_CONNECTOR = vmware.VmdkConnector
|
|
|
|
def setUp(self):
|
|
super(VmdkConnectorTestCase, self).setUp()
|
|
|
|
self._connector = vmware.VmdkConnector(None)
|
|
self._connector._ip = self.IP
|
|
self._connector._port = self.PORT
|
|
self._connector._username = self.USERNAME
|
|
self._connector._password = self.PASSWORD
|
|
self._connector._api_retry_count = self.API_RETRY_COUNT
|
|
self._connector._task_poll_interval = self.TASK_POLL_INTERVAL
|
|
self._connector._ca_file = self.CA_FILE
|
|
self._connector._insecure = True
|
|
self._connector._tmp_dir = self.TMP_DIR
|
|
self._connector._timeout = self.IMG_TX_TIMEOUT
|
|
|
|
def test_load_config(self):
|
|
config = {
|
|
'vmware_host_ip': 'localhost',
|
|
'vmware_host_port': 1234,
|
|
'vmware_host_username': 'root',
|
|
'vmware_host_password': 'pswd',
|
|
'vmware_api_retry_count': 1,
|
|
'vmware_task_poll_interval': 1.0,
|
|
'vmware_ca_file': None,
|
|
'vmware_insecure': False,
|
|
'vmware_tmp_dir': '/tmp',
|
|
'vmware_image_transfer_timeout_secs': 5,
|
|
}
|
|
self._connector._load_config({'config': config})
|
|
|
|
self.assertEqual('localhost', self._connector._ip)
|
|
self.assertEqual(1234, self._connector._port)
|
|
self.assertEqual('root', self._connector._username)
|
|
self.assertEqual('pswd', self._connector._password)
|
|
self.assertEqual(1, self._connector._api_retry_count)
|
|
self.assertEqual(1.0, self._connector._task_poll_interval)
|
|
self.assertIsNone(self._connector._ca_file)
|
|
self.assertFalse(self._connector._insecure)
|
|
self.assertEqual('/tmp', self._connector._tmp_dir)
|
|
self.assertEqual(5, self._connector._timeout)
|
|
|
|
@mock.patch('oslo_vmware.api.VMwareAPISession')
|
|
def test_create_session(self, session):
|
|
session.return_value = mock.sentinel.session
|
|
|
|
ret = self._connector._create_session()
|
|
|
|
self.assertEqual(mock.sentinel.session, ret)
|
|
session.assert_called_once_with(
|
|
self._connector._ip,
|
|
self._connector._username,
|
|
self._connector._password,
|
|
self._connector._api_retry_count,
|
|
self._connector._task_poll_interval,
|
|
port=self._connector._port,
|
|
cacert=self._connector._ca_file,
|
|
insecure=self._connector._insecure)
|
|
|
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
|
@mock.patch('tempfile.mkstemp')
|
|
@mock.patch('os.close')
|
|
def test_create_temp_file(
|
|
self, close, mkstemp, ensure_tree):
|
|
fd = mock.sentinel.fd
|
|
tmp = mock.sentinel.tmp
|
|
mkstemp.return_value = (fd, tmp)
|
|
|
|
prefix = ".vmdk"
|
|
suffix = "test"
|
|
ret = self._connector._create_temp_file(prefix=prefix, suffix=suffix)
|
|
|
|
self.assertEqual(tmp, ret)
|
|
ensure_tree.assert_called_once_with(self._connector._tmp_dir)
|
|
mkstemp.assert_called_once_with(dir=self._connector._tmp_dir,
|
|
prefix=prefix,
|
|
suffix=suffix)
|
|
close.assert_called_once_with(fd)
|
|
|
|
@mock.patch('os_brick.initiator.connectors.vmware.open', create=True)
|
|
@mock.patch('oslo_vmware.image_transfer.copy_stream_optimized_disk')
|
|
def test_download_vmdk(self, copy_disk, file_open):
|
|
file_open_ret = mock.Mock()
|
|
tmp_file = mock.sentinel.tmp_file
|
|
file_open_ret.__enter__ = mock.Mock(return_value=tmp_file)
|
|
file_open_ret.__exit__ = mock.Mock(return_value=None)
|
|
file_open.return_value = file_open_ret
|
|
|
|
tmp_file_path = mock.sentinel.tmp_file_path
|
|
session = mock.sentinel.session
|
|
backing = mock.sentinel.backing
|
|
vmdk_path = mock.sentinel.vmdk_path
|
|
vmdk_size = mock.sentinel.vmdk_size
|
|
self._connector._download_vmdk(
|
|
tmp_file_path, session, backing, vmdk_path, vmdk_size)
|
|
|
|
file_open.assert_called_once_with(tmp_file_path, 'wb')
|
|
copy_disk.assert_called_once_with(None,
|
|
self._connector._timeout,
|
|
tmp_file,
|
|
session=session,
|
|
host=self._connector._ip,
|
|
port=self._connector._port,
|
|
vm=backing,
|
|
vmdk_file_path=vmdk_path,
|
|
vmdk_size=vmdk_size)
|
|
|
|
def _create_connection_properties(self):
|
|
return {'volume_id': 'ed083474-d325-4a99-b301-269111654f0d',
|
|
'volume': 'ref-1',
|
|
'vmdk_path': '[ds] foo/bar.vmdk',
|
|
'vmdk_size': units.Gi,
|
|
'datastore': 'ds-1',
|
|
'datacenter': 'dc-1',
|
|
}
|
|
|
|
@mock.patch.object(VMDK_CONNECTOR, '_load_config')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_create_session')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_create_temp_file')
|
|
@mock.patch('oslo_vmware.vim_util.get_moref')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_download_vmdk')
|
|
@mock.patch('os.path.getmtime')
|
|
def test_connect_volume(
|
|
self, getmtime, download_vmdk, get_moref, create_temp_file,
|
|
create_session, load_config):
|
|
session = mock.Mock()
|
|
create_session.return_value = session
|
|
|
|
tmp_file_path = mock.sentinel.tmp_file_path
|
|
create_temp_file.return_value = tmp_file_path
|
|
|
|
backing = mock.sentinel.backing
|
|
get_moref.return_value = backing
|
|
|
|
last_modified = mock.sentinel.last_modified
|
|
getmtime.return_value = last_modified
|
|
|
|
props = self._create_connection_properties()
|
|
ret = self._connector.connect_volume(props)
|
|
|
|
self.assertEqual(tmp_file_path, ret['path'])
|
|
self.assertEqual(last_modified, ret['last_modified'])
|
|
load_config.assert_called_once_with(props)
|
|
create_session.assert_called_once_with()
|
|
create_temp_file.assert_called_once_with(
|
|
suffix=".vmdk", prefix=props['volume_id'])
|
|
download_vmdk.assert_called_once_with(
|
|
tmp_file_path, session, backing, props['vmdk_path'],
|
|
props['vmdk_size'])
|
|
session.logout.assert_called_once_with()
|
|
|
|
@ddt.data((None, False), ([mock.sentinel.snap], True))
|
|
@ddt.unpack
|
|
def test_snapshot_exists(self, snap_list, exp_return_value):
|
|
snapshot = mock.Mock(rootSnapshotList=snap_list)
|
|
session = mock.Mock()
|
|
session.invoke_api.return_value = snapshot
|
|
|
|
backing = mock.sentinel.backing
|
|
ret = self._connector._snapshot_exists(session, backing)
|
|
|
|
self.assertEqual(exp_return_value, ret)
|
|
session.invoke_api.assert_called_once_with(
|
|
vim_util, 'get_object_property', session.vim, backing, 'snapshot')
|
|
|
|
def test_create_temp_ds_folder(self):
|
|
session = mock.Mock()
|
|
ds_folder_path = mock.sentinel.ds_folder_path
|
|
dc_ref = mock.sentinel.dc_ref
|
|
self._connector._create_temp_ds_folder(session, ds_folder_path, dc_ref)
|
|
|
|
session.invoke_api.assert_called_once_with(
|
|
session.vim,
|
|
'MakeDirectory',
|
|
session.vim.service_content.fileManager,
|
|
name=ds_folder_path,
|
|
datacenter=dc_ref)
|
|
|
|
@mock.patch('oslo_vmware.objects.datastore.get_datastore_by_ref')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_create_temp_ds_folder')
|
|
@mock.patch('os_brick.initiator.connectors.vmware.open', create=True)
|
|
@mock.patch.object(VMDK_CONNECTOR, '_upload_vmdk')
|
|
@mock.patch('os.path.getsize')
|
|
def test_disconnect(
|
|
self, getsize, upload_vmdk, file_open, create_temp_ds_folder,
|
|
get_ds_by_ref):
|
|
ds_ref = mock.sentinel.ds_ref
|
|
ds_name = 'datastore-1'
|
|
dstore = datastore.Datastore(ds_ref, ds_name)
|
|
get_ds_by_ref.return_value = dstore
|
|
|
|
file_open_ret = mock.Mock()
|
|
tmp_file = mock.sentinel.tmp_file
|
|
file_open_ret.__enter__ = mock.Mock(return_value=tmp_file)
|
|
file_open_ret.__exit__ = mock.Mock(return_value=None)
|
|
file_open.return_value = file_open_ret
|
|
|
|
dc_name = mock.sentinel.dc_name
|
|
delete_task = mock.sentinel.delete_vdisk_task
|
|
copy_task = mock.sentinel.copy_vdisk_task
|
|
delete_file_task = mock.sentinel.delete_file_task
|
|
session = mock.Mock()
|
|
session.invoke_api.side_effect = [
|
|
dc_name, delete_task, copy_task, delete_file_task]
|
|
|
|
getsize.return_value = units.Gi
|
|
|
|
tmp_file_path = '/tmp/foo.vmdk'
|
|
dc_ref = mock.sentinel.dc_ref
|
|
vmdk_path = mock.sentinel.vmdk_path
|
|
self._connector._disconnect(
|
|
tmp_file_path, session, ds_ref, dc_ref, vmdk_path)
|
|
|
|
tmp_folder_path = self._connector.TMP_IMAGES_DATASTORE_FOLDER_PATH
|
|
ds_folder_path = '[%s] %s' % (ds_name, tmp_folder_path)
|
|
create_temp_ds_folder.assert_called_once_with(
|
|
session, ds_folder_path, dc_ref)
|
|
file_open.assert_called_once_with(tmp_file_path, "rb")
|
|
|
|
self.assertEqual(
|
|
mock.call(vim_util, 'get_object_property', session.vim, dc_ref,
|
|
'name'), session.invoke_api.call_args_list[0])
|
|
|
|
exp_rel_path = '%s/foo.vmdk' % tmp_folder_path
|
|
upload_vmdk.assert_called_once_with(
|
|
tmp_file, self._connector._ip, self._connector._port, dc_name,
|
|
ds_name, session.vim.client.options.transport.cookiejar,
|
|
exp_rel_path, units.Gi, self._connector._ca_file,
|
|
self._connector._timeout)
|
|
|
|
disk_mgr = session.vim.service_content.virtualDiskManager
|
|
self.assertEqual(
|
|
mock.call(session.vim, 'DeleteVirtualDisk_Task', disk_mgr,
|
|
name=vmdk_path, datacenter=dc_ref),
|
|
session.invoke_api.call_args_list[1])
|
|
self.assertEqual(mock.call(delete_task),
|
|
session.wait_for_task.call_args_list[0])
|
|
|
|
src = '[%s] %s' % (ds_name, exp_rel_path)
|
|
self.assertEqual(
|
|
mock.call(session.vim, 'CopyVirtualDisk_Task', disk_mgr,
|
|
sourceName=src, sourceDatacenter=dc_ref,
|
|
destName=vmdk_path, destDatacenter=dc_ref),
|
|
session.invoke_api.call_args_list[2])
|
|
self.assertEqual(mock.call(copy_task),
|
|
session.wait_for_task.call_args_list[1])
|
|
|
|
file_mgr = session.vim.service_content.fileManager
|
|
self.assertEqual(
|
|
mock.call(session.vim, 'DeleteDatastoreFile_Task', file_mgr,
|
|
name=src, datacenter=dc_ref),
|
|
session.invoke_api.call_args_list[3])
|
|
self.assertEqual(mock.call(delete_file_task),
|
|
session.wait_for_task.call_args_list[2])
|
|
|
|
@mock.patch('os.path.exists')
|
|
def test_disconnect_volume_with_missing_temp_file(self, path_exists):
|
|
path_exists.return_value = False
|
|
|
|
path = mock.sentinel.path
|
|
self.assertRaises(exception.NotFound,
|
|
self._connector.disconnect_volume,
|
|
mock.ANY,
|
|
{'path': path})
|
|
path_exists.assert_called_once_with(path)
|
|
|
|
@mock.patch('os.path.exists')
|
|
@mock.patch('os.path.getmtime')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_disconnect')
|
|
@mock.patch('os.remove')
|
|
def test_disconnect_volume_with_unmodified_file(
|
|
self, remove, disconnect, getmtime, path_exists):
|
|
path_exists.return_value = True
|
|
|
|
mtime = 1467802060
|
|
getmtime.return_value = mtime
|
|
|
|
path = mock.sentinel.path
|
|
self._connector.disconnect_volume(mock.ANY, {'path': path,
|
|
'last_modified': mtime})
|
|
|
|
path_exists.assert_called_once_with(path)
|
|
getmtime.assert_called_once_with(path)
|
|
disconnect.assert_not_called()
|
|
remove.assert_called_once_with(path)
|
|
|
|
@mock.patch('os.path.exists')
|
|
@mock.patch('os.path.getmtime')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_load_config')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_create_session')
|
|
@mock.patch('oslo_vmware.vim_util.get_moref')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_snapshot_exists')
|
|
@mock.patch.object(VMDK_CONNECTOR, '_disconnect')
|
|
@mock.patch('os.remove')
|
|
def test_disconnect_volume(
|
|
self, remove, disconnect, snapshot_exists, get_moref,
|
|
create_session, load_config, getmtime, path_exists):
|
|
path_exists.return_value = True
|
|
|
|
mtime = 1467802060
|
|
getmtime.return_value = mtime
|
|
|
|
session = mock.Mock()
|
|
create_session.return_value = session
|
|
|
|
snapshot_exists.return_value = False
|
|
|
|
backing = mock.sentinel.backing
|
|
ds_ref = mock.sentinel.ds_ref
|
|
dc_ref = mock.sentinel.dc_ref
|
|
get_moref.side_effect = [backing, ds_ref, dc_ref]
|
|
|
|
props = self._create_connection_properties()
|
|
path = mock.sentinel.path
|
|
self._connector.disconnect_volume(props, {'path': path,
|
|
'last_modified': mtime - 1})
|
|
|
|
path_exists.assert_called_once_with(path)
|
|
getmtime.assert_called_once_with(path)
|
|
load_config.assert_called_once_with(props)
|
|
create_session.assert_called_once_with()
|
|
snapshot_exists.assert_called_once_with(session, backing)
|
|
disconnect.assert_called_once_with(
|
|
path, session, ds_ref, dc_ref, props['vmdk_path'])
|
|
remove.assert_called_once_with(path)
|
|
session.logout.assert_called_once_with()
|