Merge "Add connector for vmdk volumes"
This commit is contained in:
@@ -56,3 +56,4 @@ QUOBYTE = "QUOBYTE"
|
||||
DISCO = "DISCO"
|
||||
VZSTORAGE = "VZSTORAGE"
|
||||
SHEEPDOG = "SHEEPDOG"
|
||||
VMDK = "VMDK"
|
||||
|
||||
@@ -90,6 +90,7 @@ connector_list = [
|
||||
'os_brick.initiator.connectors.hgst.HGSTConnector',
|
||||
'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
|
||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
||||
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
||||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
@@ -135,6 +136,8 @@ _connector_mapping_linux = {
|
||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
||||
initiator.SHEEPDOG:
|
||||
'os_brick.initiator.connectors.sheepdog.SheepdogConnector',
|
||||
initiator.VMDK:
|
||||
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
||||
}
|
||||
|
||||
# Mapping for the S390X platform
|
||||
|
||||
276
os_brick/initiator/connectors/vmware.py
Normal file
276
os_brick/initiator/connectors/vmware.py
Normal file
@@ -0,0 +1,276 @@
|
||||
# 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 os
|
||||
import tempfile
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import fileutils
|
||||
try:
|
||||
from oslo_vmware import api
|
||||
from oslo_vmware import exceptions as oslo_vmw_exceptions
|
||||
from oslo_vmware import image_transfer
|
||||
from oslo_vmware.objects import datastore
|
||||
from oslo_vmware import rw_handles
|
||||
from oslo_vmware import vim_util
|
||||
except ImportError:
|
||||
vim_util = None
|
||||
import six
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _
|
||||
from os_brick.initiator import initiator_connector
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VmdkConnector(initiator_connector.InitiatorConnector):
|
||||
"""Connector for volumes created by the VMDK driver.
|
||||
|
||||
This connector is only used for backup and restore of Cinder volumes.
|
||||
"""
|
||||
|
||||
TMP_IMAGES_DATASTORE_FOLDER_PATH = "cinder_temp"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Check if oslo.vmware library is available.
|
||||
if vim_util is None:
|
||||
message = _("Missing oslo_vmware python module, ensure oslo.vmware"
|
||||
" library is installed and available.")
|
||||
raise exception.BrickException(message=message)
|
||||
|
||||
super(VmdkConnector, self).__init__(*args, **kwargs)
|
||||
|
||||
self._ip = None
|
||||
self._port = None
|
||||
self._username = None
|
||||
self._password = None
|
||||
self._api_retry_count = None
|
||||
self._task_poll_interval = None
|
||||
self._ca_file = None
|
||||
self._insecure = None
|
||||
self._tmp_dir = None
|
||||
self._timeout = None
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(root_helper, *args, **kwargs):
|
||||
return {}
|
||||
|
||||
def check_valid_device(self, path, run_as_root=True):
|
||||
pass
|
||||
|
||||
def get_volume_paths(self, connection_properties):
|
||||
return []
|
||||
|
||||
def get_search_path(self):
|
||||
return None
|
||||
|
||||
def get_all_available_volumes(self, connection_properties=None):
|
||||
pass
|
||||
|
||||
def _load_config(self, connection_properties):
|
||||
config = connection_properties['config']
|
||||
self._ip = config['vmware_host_ip']
|
||||
self._port = config['vmware_host_port']
|
||||
self._username = config['vmware_host_username']
|
||||
self._password = config['vmware_host_password']
|
||||
self._api_retry_count = config['vmware_api_retry_count']
|
||||
self._task_poll_interval = config['vmware_task_poll_interval']
|
||||
self._ca_file = config['vmware_ca_file']
|
||||
self._insecure = config['vmware_insecure']
|
||||
self._tmp_dir = config['vmware_tmp_dir']
|
||||
self._timeout = config['vmware_image_transfer_timeout_secs']
|
||||
|
||||
def _create_session(self):
|
||||
return api.VMwareAPISession(self._ip,
|
||||
self._username,
|
||||
self._password,
|
||||
self._api_retry_count,
|
||||
self._task_poll_interval,
|
||||
port=self._port,
|
||||
cacert=self._ca_file,
|
||||
insecure=self._insecure)
|
||||
|
||||
def _create_temp_file(self, *args, **kwargs):
|
||||
fileutils.ensure_tree(self._tmp_dir)
|
||||
fd, tmp = tempfile.mkstemp(dir=self._tmp_dir, *args, **kwargs)
|
||||
os.close(fd)
|
||||
return tmp
|
||||
|
||||
def _download_vmdk(
|
||||
self, tmp_file_path, session, backing, vmdk_path, vmdk_size):
|
||||
with open(tmp_file_path, "wb") as tmp_file:
|
||||
image_transfer.copy_stream_optimized_disk(
|
||||
None,
|
||||
self._timeout,
|
||||
tmp_file,
|
||||
session=session,
|
||||
host=self._ip,
|
||||
port=self._port,
|
||||
vm=backing,
|
||||
vmdk_file_path=vmdk_path,
|
||||
vmdk_size=vmdk_size)
|
||||
|
||||
def connect_volume(self, connection_properties):
|
||||
# Download the volume vmdk from vCenter server to a temporary file
|
||||
# and return its path.
|
||||
self._load_config(connection_properties)
|
||||
session = self._create_session()
|
||||
|
||||
tmp_file_path = self._create_temp_file(
|
||||
suffix=".vmdk", prefix=connection_properties['volume_id'])
|
||||
backing = vim_util.get_moref(connection_properties['volume'],
|
||||
"VirtualMachine")
|
||||
vmdk_path = connection_properties['vmdk_path']
|
||||
vmdk_size = connection_properties['vmdk_size']
|
||||
try:
|
||||
self._download_vmdk(
|
||||
tmp_file_path, session, backing, vmdk_path, vmdk_size)
|
||||
finally:
|
||||
session.logout()
|
||||
|
||||
# Save the last modified time of the temporary so that we can decide
|
||||
# whether to upload the file back to vCenter server during disconnect.
|
||||
last_modified = os.path.getmtime(tmp_file_path)
|
||||
return {'path': tmp_file_path, 'last_modified': last_modified}
|
||||
|
||||
def _snapshot_exists(self, session, backing):
|
||||
snapshot = session.invoke_api(vim_util,
|
||||
'get_object_property',
|
||||
session.vim,
|
||||
backing,
|
||||
'snapshot')
|
||||
if snapshot is None or snapshot.rootSnapshotList is None:
|
||||
return False
|
||||
return len(snapshot.rootSnapshotList) != 0
|
||||
|
||||
def _create_temp_ds_folder(self, session, ds_folder_path, dc_ref):
|
||||
fileManager = session.vim.service_content.fileManager
|
||||
try:
|
||||
session.invoke_api(session.vim,
|
||||
'MakeDirectory',
|
||||
fileManager,
|
||||
name=ds_folder_path,
|
||||
datacenter=dc_ref)
|
||||
except oslo_vmw_exceptions.FileAlreadyExistsException:
|
||||
pass
|
||||
|
||||
# Note(vbala) remove this method when we implement it in oslo.vmware
|
||||
def _upload_vmdk(
|
||||
self, read_handle, host, port, dc_name, ds_name, cookies,
|
||||
upload_file_path, file_size, cacerts, timeout_secs):
|
||||
write_handle = rw_handles.FileWriteHandle(host,
|
||||
port,
|
||||
dc_name,
|
||||
ds_name,
|
||||
cookies,
|
||||
upload_file_path,
|
||||
file_size,
|
||||
cacerts=cacerts)
|
||||
image_transfer._start_transfer(read_handle, write_handle, timeout_secs)
|
||||
|
||||
def _disconnect(self, tmp_file_path, session, ds_ref, dc_ref, vmdk_path):
|
||||
# The restored volume is in compressed (streamOptimized) format.
|
||||
# So we upload it to a temporary location in vCenter datastore and copy
|
||||
# the compressed vmdk to the volume vmdk. The copy operation
|
||||
# decompresses the disk to a format suitable for attaching to Nova
|
||||
# instances in vCenter.
|
||||
dstore = datastore.get_datastore_by_ref(session, ds_ref)
|
||||
ds_path = dstore.build_path(
|
||||
VmdkConnector.TMP_IMAGES_DATASTORE_FOLDER_PATH,
|
||||
os.path.basename(tmp_file_path))
|
||||
self._create_temp_ds_folder(
|
||||
session, six.text_type(ds_path.parent), dc_ref)
|
||||
|
||||
with open(tmp_file_path, "rb") as tmp_file:
|
||||
dc_name = session.invoke_api(
|
||||
vim_util, 'get_object_property', session.vim, dc_ref, 'name')
|
||||
cookies = session.vim.client.options.transport.cookiejar
|
||||
cacerts = self._ca_file if self._ca_file else not self._insecure
|
||||
self._upload_vmdk(
|
||||
tmp_file, self._ip, self._port, dc_name, dstore.name, cookies,
|
||||
ds_path.rel_path, os.path.getsize(tmp_file_path), cacerts,
|
||||
self._timeout)
|
||||
|
||||
# Delete the current volume vmdk because the copy operation does not
|
||||
# overwrite.
|
||||
LOG.debug("Deleting %s", vmdk_path)
|
||||
disk_mgr = session.vim.service_content.virtualDiskManager
|
||||
task = session.invoke_api(session.vim,
|
||||
'DeleteVirtualDisk_Task',
|
||||
disk_mgr,
|
||||
name=vmdk_path,
|
||||
datacenter=dc_ref)
|
||||
session.wait_for_task(task)
|
||||
|
||||
src = six.text_type(ds_path)
|
||||
LOG.debug("Copying %(src)s to %(dest)s", {'src': src,
|
||||
'dest': vmdk_path})
|
||||
task = session.invoke_api(session.vim,
|
||||
'CopyVirtualDisk_Task',
|
||||
disk_mgr,
|
||||
sourceName=src,
|
||||
sourceDatacenter=dc_ref,
|
||||
destName=vmdk_path,
|
||||
destDatacenter=dc_ref)
|
||||
session.wait_for_task(task)
|
||||
|
||||
# Delete the compressed vmdk at the temporary location.
|
||||
LOG.debug("Deleting %s", src)
|
||||
file_mgr = session.vim.service_content.fileManager
|
||||
task = session.invoke_api(session.vim,
|
||||
'DeleteDatastoreFile_Task',
|
||||
file_mgr,
|
||||
name=src,
|
||||
datacenter=dc_ref)
|
||||
session.wait_for_task(task)
|
||||
|
||||
def disconnect_volume(self, connection_properties, device_info):
|
||||
tmp_file_path = device_info['path']
|
||||
if not os.path.exists(tmp_file_path):
|
||||
msg = _("Vmdk: %s not found.") % tmp_file_path
|
||||
raise exception.NotFound(message=msg)
|
||||
|
||||
session = None
|
||||
try:
|
||||
# We upload the temporary file to vCenter server only if it is
|
||||
# modified after connect_volume.
|
||||
if os.path.getmtime(tmp_file_path) > device_info['last_modified']:
|
||||
self._load_config(connection_properties)
|
||||
session = self._create_session()
|
||||
backing = vim_util.get_moref(connection_properties['volume'],
|
||||
"VirtualMachine")
|
||||
# Currently there is no way we can restore the volume if it
|
||||
# contains redo-log based snapshots (bug 1599026).
|
||||
if self._snapshot_exists(session, backing):
|
||||
msg = (_("Backing of volume: %s contains one or more "
|
||||
"snapshots; cannot disconnect.") %
|
||||
connection_properties['volume_id'])
|
||||
raise exception.BrickException(message=msg)
|
||||
|
||||
ds_ref = vim_util.get_moref(
|
||||
connection_properties['datastore'], "Datastore")
|
||||
dc_ref = vim_util.get_moref(
|
||||
connection_properties['datacenter'], "Datacenter")
|
||||
vmdk_path = connection_properties['vmdk_path']
|
||||
self._disconnect(
|
||||
tmp_file_path, session, ds_ref, dc_ref, vmdk_path)
|
||||
finally:
|
||||
os.remove(tmp_file_path)
|
||||
if session:
|
||||
session.logout()
|
||||
|
||||
def extend_volume(self, connection_properties):
|
||||
raise NotImplementedError
|
||||
366
os_brick/tests/initiator/connectors/test_vmware.py
Normal file
366
os_brick/tests/initiator/connectors/test_vmware.py
Normal file
@@ -0,0 +1,366 @@
|
||||
# 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()
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Added initiator connector 'VmdkConnector' to support backup and
|
||||
restore of vmdk volumes by Cinder backup service.
|
||||
@@ -14,3 +14,4 @@ testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
os-testr>=0.7.0 # Apache-2.0
|
||||
oslo.vmware>=2.11.0 # Apache-2.0
|
||||
|
||||
Reference in New Issue
Block a user