Add connector for vmdk volumes
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
This commit is contained in:
parent
f38c6c87be
commit
9f70ace94c
@ -56,3 +56,4 @@ QUOBYTE = "QUOBYTE"
|
|||||||
DISCO = "DISCO"
|
DISCO = "DISCO"
|
||||||
VZSTORAGE = "VZSTORAGE"
|
VZSTORAGE = "VZSTORAGE"
|
||||||
SHEEPDOG = "SHEEPDOG"
|
SHEEPDOG = "SHEEPDOG"
|
||||||
|
VMDK = "VMDK"
|
||||||
|
@ -90,6 +90,7 @@ connector_list = [
|
|||||||
'os_brick.initiator.connectors.hgst.HGSTConnector',
|
'os_brick.initiator.connectors.hgst.HGSTConnector',
|
||||||
'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
|
'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
|
||||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
'os_brick.initiator.connectors.disco.DISCOConnector',
|
||||||
|
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
||||||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
||||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||||
@ -135,6 +136,8 @@ _connector_mapping_linux = {
|
|||||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
'os_brick.initiator.connectors.disco.DISCOConnector',
|
||||||
initiator.SHEEPDOG:
|
initiator.SHEEPDOG:
|
||||||
'os_brick.initiator.connectors.sheepdog.SheepdogConnector',
|
'os_brick.initiator.connectors.sheepdog.SheepdogConnector',
|
||||||
|
initiator.VMDK:
|
||||||
|
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mapping for the S390X platform
|
# 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
|
testscenarios>=0.4 # Apache-2.0/BSD
|
||||||
testtools>=1.4.0 # MIT
|
testtools>=1.4.0 # MIT
|
||||||
os-testr>=0.7.0 # Apache-2.0
|
os-testr>=0.7.0 # Apache-2.0
|
||||||
|
oslo.vmware>=2.11.0 # Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user