summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-02-20 23:06:23 +0000
committerGerrit Code Review <review@openstack.org>2014-02-20 23:06:23 +0000
commitc5fba2681e32b5f30d5dc55c6da2881e09fe2cb1 (patch)
tree810c14edd6e21c774b294a7ed1f8875be04ec7a3
parentc08eb22ece41c676964340eb42ed9c89c85fb026 (diff)
parent9f6258bebf3df693e5175ec4ac9d1d248333d223 (diff)
downloadoslo-vmware-c5fba2681e32b5f30d5dc55c6da2881e09fe2cb1.tar.gz
Merge "Move read/write handles in VMware drivers to OSLO"
-rw-r--r--oslo/vmware/rw_handles.py653
-rw-r--r--tests/test_rw_handles.py300
2 files changed, 953 insertions, 0 deletions
diff --git a/oslo/vmware/rw_handles.py b/oslo/vmware/rw_handles.py
new file mode 100644
index 0000000..ba1c7fe
--- /dev/null
+++ b/oslo/vmware/rw_handles.py
@@ -0,0 +1,653 @@
+# Copyright (c) 2014 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.
+
+"""
+Classes defining read and write handles for image transfer.
+
+This module defines various classes for reading and writing files including
+VMDK files in VMware servers. It also contains a class to read images from
+glance server.
+"""
+
+import httplib
+import socket
+import urllib
+import urllib2
+import urlparse
+
+import netaddr
+
+from oslo.vmware import exceptions
+from oslo.vmware.openstack.common.gettextutils import _
+from oslo.vmware.openstack.common import log as logging
+from oslo.vmware import vim_util
+
+
+LOG = logging.getLogger(__name__)
+
+READ_CHUNKSIZE = 65536
+USER_AGENT = 'OpenStack-ESX-Adapter'
+
+
+class FileHandle(object):
+ """Base class for VMware server file (including VMDK) access over HTTP.
+
+ This class wraps a backing file handle and provides utility methods
+ for various sub-classes.
+ """
+
+ def __init__(self, file_handle):
+ """Initializes the file handle.
+
+ :param _file_handle: backing file handle
+ """
+ self._eof = False
+ self._file_handle = file_handle
+
+ def close(self):
+ """Close the file handle."""
+ try:
+ self._file_handle.close()
+ except Exception:
+ LOG.warn(_("Error occurred while closing the file handle"),
+ exc_info=True)
+
+ def __del__(self):
+ """Close the file handle on garbage collection."""
+ self.close()
+
+ def _build_vim_cookie_header(self, vim_cookies):
+ """Build ESX host session cookie header."""
+ cookie_header = ""
+ for vim_cookie in vim_cookies:
+ cookie_header = vim_cookie.name + '=' + vim_cookie.value
+ break
+ return cookie_header
+
+ def write(self, data):
+ """Write data to the file.
+
+ :param data: data to be written
+ :raises: NotImplementedError
+ """
+ raise NotImplementedError()
+
+ def read(self, chunk_size):
+ """Read a chunk of data.
+
+ :param chunk_size: read chunk size
+ :raises: NotImplementedError
+ """
+ raise NotImplementedError()
+
+ def get_size(self):
+ """Get size of the file to be read.
+
+ :raises: NotImplementedError
+ """
+ raise NotImplementedError()
+
+ def _is_valid_ipv6(self, address):
+ """Checks whether the given host address is a valid IPv6 address."""
+ try:
+ return netaddr.valid_ipv6(address)
+ except Exception:
+ return False
+
+ def _get_soap_url(self, scheme, host):
+ """Returns the IPv4/v6 compatible SOAP URL for the given host."""
+ if self._is_valid_ipv6(host):
+ return '%s://[%s]' % (scheme, host)
+ return '%s://%s' % (scheme, host)
+
+ def _fix_esx_url(self, url, host):
+ """Fix netloc in the case of an ESX host.
+
+ In the case of an ESX host, the netloc is set to '*' in the URL
+ returned in HttpNfcLeaseInfo. It should be replaced with host name
+ or IP address.
+ """
+ urlp = urlparse.urlparse(url)
+ if urlp.netloc == '*':
+ scheme, netloc, path, params, query, fragment = urlp
+ url = urlparse.urlunparse((scheme,
+ host,
+ path,
+ params,
+ query,
+ fragment))
+ return url
+
+ def _find_vmdk_url(self, lease_info, host):
+ """Find the URL corresponding to a VMDK file in lease info."""
+ LOG.debug(_("Finding VMDK URL from lease info."))
+ url = None
+ for deviceUrl in lease_info.deviceUrl:
+ if deviceUrl.disk:
+ url = self._fix_esx_url(deviceUrl.url, host)
+ break
+ if not url:
+ excep_msg = _("Could not retrieve VMDK URL from lease info.")
+ LOG.error(excep_msg)
+ raise exceptions.VimException(excep_msg)
+ LOG.debug(_("Found VMDK URL: %s from lease info."), url)
+ return url
+
+
+class FileWriteHandle(FileHandle):
+ """Write handle for a file in VMware server."""
+
+ def __init__(self, host, data_center_name, datastore_name, cookies,
+ file_path, file_size, scheme='https'):
+ """Initializes the write handle with given parameters.
+
+ :param host: ESX/VC server IP address[:port] or host name[:port]
+ :param data_center_name: name of the data center in the case of a VC
+ server
+ :param datastore_name: name of the datastore where the file is stored
+ :param cookies: cookies to build the vim cookie header
+ :param file_path: datastore path where the file is written
+ :param file_size: size of the file in bytes
+ :param scheme: protocol-- http or https
+ :raises: VimConnectionException, ValueError
+ """
+ soap_url = self._get_soap_url(scheme, host)
+ param_list = {'dcPath': data_center_name, 'dsName': datastore_name}
+ self._url = '%s/folder/%s' % (soap_url, file_path)
+ self._url = self._url + '?' + urllib.urlencode(param_list)
+
+ self.conn = self._create_connection(self._url,
+ file_size,
+ cookies)
+ FileHandle.__init__(self, self.conn)
+
+ def _create_connection(self, url, file_size, cookies):
+ """Create HTTP connection to write to the file with given URL."""
+ LOG.debug(_("Creating HTTP connection to write to file with "
+ "size = %(file_size)d and URL = %(url)s."),
+ {'file_size': file_size,
+ 'url': url})
+ _urlparse = urlparse.urlparse(url)
+ scheme, netloc, path, params, query, fragment = _urlparse
+
+ try:
+ if scheme == 'http':
+ conn = httplib.HTTPConnection(netloc)
+ elif scheme == 'https':
+ conn = httplib.HTTPSConnection(netloc)
+ else:
+ excep_msg = _("Invalid scheme: %s.") % scheme
+ LOG.error(excep_msg)
+ raise ValueError(excep_msg)
+
+ conn.putrequest('PUT', path + '?' + query)
+ conn.putheader('User-Agent', USER_AGENT)
+ conn.putheader('Content-Length', file_size)
+ conn.putheader('Cookie', self._build_vim_cookie_header(cookies))
+ conn.endheaders()
+ LOG.debug(_("Created HTTP connection to write to file with "
+ "URL = %s."), url)
+ return conn
+ except (httplib.InvalidURL, httplib.CannotSendRequest,
+ httplib.CannotSendHeader) as excep:
+ excep_msg = _("Error occurred while creating HTTP connection "
+ "to write to file with URL = %s.") % url
+ LOG.exception(excep_msg)
+ raise exceptions.VimConnectionException(excep_msg, excep)
+
+ def write(self, data):
+ """Write data to the file.
+
+ :param data: data to be written
+ :raises: VimConnectionException, VimException
+ """
+ LOG.debug(_("Writing data to %s."), self._url)
+ try:
+ self._file_handle.send(data)
+ except (socket.error, httplib.NotConnected) as excep:
+ excep_msg = _("Connection error occurred while writing data to"
+ " %s.") % self._url
+ LOG.exception(excep_msg)
+ raise exceptions.VimConnectionException(excep_msg, excep)
+ except Exception as excep:
+ # TODO(vbala) We need to catch and raise specific exceptions
+ # related to connection problems, invalid request and invalid
+ # arguments.
+ excep_msg = _("Error occurred while writing data to"
+ " %s.") % self._url
+ LOG.exception(excep_msg)
+ raise exceptions.VimException(excep_msg, excep)
+
+ def close(self):
+ """Get the response and close the connection."""
+ LOG.debug(_("Closing write handle for %s."), self._url)
+ try:
+ self.conn.getresponse()
+ except Exception:
+ LOG.warn(_("Error occurred while reading the HTTP response."),
+ exc_info=True)
+ super(FileWriteHandle, self).close()
+ LOG.debug(_("Closed write handle for %s."), self._url)
+
+ def __str__(self):
+ return "File write handle for %s" % self._url
+
+
+class VmdkWriteHandle(FileHandle):
+ """VMDK write handle based on HttpNfcLease.
+
+ This class creates a vApp in the specified resource pool and uploads the
+ virtual disk contents.
+ """
+
+ def __init__(self, session, host, rp_ref, vm_folder_ref, import_spec,
+ vmdk_size):
+ """Initializes the VMDK write handle with input parameters.
+
+ :param session: valid API session to ESX/VC server
+ :param host: ESX/VC server IP address[:port] or host name[:port]
+ :param rp_ref: resource pool into which the backing VM is imported
+ :param vm_folder_ref: VM folder in ESX/VC inventory to use as parent
+ of backing VM
+ :param import_spec: import specification of the backing VM
+ :param vmdk_size: size of the backing VM's VMDK file
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException,
+ ValueError
+ """
+ self._session = session
+ self._vmdk_size = vmdk_size
+ self._bytes_written = 0
+
+ # Get lease and its info for vApp import
+ self._lease = self._create_and_wait_for_lease(session,
+ rp_ref,
+ import_spec,
+ vm_folder_ref)
+ LOG.debug(_("Invoking VIM API for reading info of lease: %s."),
+ self._lease)
+ lease_info = session.invoke_api(vim_util,
+ 'get_object_property',
+ session.vim,
+ self._lease,
+ 'info')
+
+ # Find VMDK URL where data is to be written
+ self._url = self._find_vmdk_url(lease_info, host)
+ self._vm_ref = lease_info.entity
+
+ # Create HTTP connection to write to VMDK URL
+ self._conn = self._create_connection(session, self._url, vmdk_size)
+ FileHandle.__init__(self, self._conn)
+
+ def get_imported_vm(self):
+ """"Get managed object reference of the VM created for import."""
+ return self._vm_ref
+
+ def _create_and_wait_for_lease(self, session, rp_ref, import_spec,
+ vm_folder_ref):
+ """Create and wait for HttpNfcLease lease for vApp import."""
+ LOG.debug(_("Creating HttpNfcLease lease for vApp import into resource"
+ " pool: %s."),
+ rp_ref)
+ lease = session.invoke_api(session.vim,
+ 'ImportVApp',
+ rp_ref,
+ spec=import_spec,
+ folder=vm_folder_ref)
+ LOG.debug(_("Lease: %(lease)s obtained for vApp import into resource"
+ " pool %(rp_ref)s."),
+ {'lease': lease,
+ 'rp_ref': rp_ref})
+ session.wait_for_lease_ready(lease)
+ return lease
+
+ def _create_connection(self, session, url, vmdk_size):
+ """Create HTTP connection to write to VMDK file."""
+ LOG.debug(_("Creating HTTP connection to write to VMDK file with "
+ "size = %(vmdk_size)d and URL = %(url)s."),
+ {'vmdk_size': vmdk_size,
+ 'url': url})
+ cookies = session.vim.client.options.transport.cookiejar
+ _urlparse = urlparse.urlparse(url)
+ scheme, netloc, path, params, query, fragment = _urlparse
+
+ try:
+ if scheme == 'http':
+ conn = httplib.HTTPConnection(netloc)
+ elif scheme == 'https':
+ conn = httplib.HTTPSConnection(netloc)
+ else:
+ excep_msg = _("Invalid scheme: %s.") % scheme
+ LOG.error(excep_msg)
+ raise ValueError(excep_msg)
+
+ if query:
+ path = path + '?' + query
+ conn.putrequest('PUT', path)
+ conn.putheader('User-Agent', USER_AGENT)
+ conn.putheader('Content-Length', str(vmdk_size))
+ conn.putheader('Overwrite', 't')
+ conn.putheader('Cookie', self._build_vim_cookie_header(cookies))
+ conn.putheader('Content-Type', 'binary/octet-stream')
+ conn.endheaders()
+ LOG.debug(_("Created HTTP connection to write to VMDK file with "
+ "URL = %s."),
+ url)
+ return conn
+ except (httplib.InvalidURL, httplib.CannotSendRequest,
+ httplib.CannotSendHeader) as excep:
+ excep_msg = _("Error occurred while creating HTTP connection "
+ "to write to VMDK file with URL = %s.") % url
+ LOG.exception(excep_msg)
+ raise exceptions.VimConnectionException(excep_msg, excep)
+
+ def write(self, data):
+ """Write data to the file.
+
+ :param data: data to be written
+ :raises: VimConnectionException, VimException
+ """
+ LOG.debug(_("Writing data to VMDK file with URL = %s."), self._url)
+
+ try:
+ self._file_handle.send(data)
+ self._bytes_written += len(data)
+ LOG.debug(_("Total %(bytes_written)d bytes written to VMDK file "
+ "with URL = %(url)s."),
+ {'bytes_written': self._bytes_written,
+ 'url': self._url})
+ except (socket.error, httplib.NotConnected) as excep:
+ excep_msg = _("Connection error occurred while writing data to"
+ " %s.") % self._url
+ LOG.exception(excep_msg)
+ raise exceptions.VimConnectionException(excep_msg, excep)
+ except Exception as excep:
+ # TODO(vbala) We need to catch and raise specific exceptions
+ # related to connection problems, invalid request and invalid
+ # arguments.
+ excep_msg = _("Error occurred while writing data to"
+ " %s.") % self._url
+ LOG.exception(excep_msg)
+ raise exceptions.VimException(excep_msg, excep)
+
+ def update_progress(self):
+ """Updates progress to lease.
+
+ This call back to the lease is essential to keep the lease alive
+ across long running write operations.
+
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException
+ """
+ percent = int(float(self._bytes_written) / self._vmdk_size * 100)
+ LOG.debug(_("Calling VIM API to update write progress of VMDK file"
+ " with URL = %(url)s to %(percent)d%%."),
+ {'url': self._url,
+ 'percent': percent})
+ try:
+ self._session.invoke_api(self._session.vim,
+ 'HttpNfcLeaseProgress',
+ self._lease,
+ percent=percent)
+ LOG.debug(_("Updated write progress of VMDK file with "
+ "URL = %(url)s to %(percent)d%%."),
+ {'url': self._url,
+ 'percent': percent})
+ except exceptions.VimException as excep:
+ LOG.exception(_("Error occurred while updating the write progress "
+ "of VMDK file with URL = %s."),
+ self._url)
+ raise excep
+
+ def close(self):
+ """Releases the lease and close the connection.
+
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException
+ """
+ LOG.debug(_("Getting lease state for %s."), self._url)
+ try:
+ state = self._session.invoke_api(vim_util,
+ 'get_object_property',
+ self._session.vim,
+ self._lease,
+ 'state')
+ LOG.debug(_("Lease for %(url)s is in state: %(state)s."),
+ {'url': self._url,
+ 'state': state})
+ if state == 'ready':
+ LOG.debug(_("Releasing lease for %s."), self._url)
+ self._session.invoke_api(self._session.vim,
+ 'HttpNfcLeaseComplete',
+ self._lease)
+ LOG.debug(_("Lease for %s released."), self._url)
+ else:
+ LOG.debug(_("Lease for %(url)s is in state: %(state)s; no "
+ "need to release."),
+ {'url': self._url,
+ 'state': state})
+ except exceptions.VimException:
+ LOG.warn(_("Error occurred while releasing the lease for %s."),
+ self._url,
+ exc_info=True)
+ super(VmdkWriteHandle, self).close()
+ LOG.debug(_("Closed VMDK write handle for %s."), self._url)
+
+ def __str__(self):
+ return "VMDK write handle for %s" % self._url
+
+
+class VmdkReadHandle(FileHandle):
+ """VMDK read handle based on HttpNfcLease."""
+
+ def __init__(self, session, host, vm_ref, vmdk_path, vmdk_size):
+ """Initializes the VMDK read handle with the given parameters.
+
+ During the read (export) operation, the VMDK file is converted to a
+ stream-optimized sparse disk format. Therefore, the size of the VMDK
+ file read may be smaller than the actual VMDK size.
+
+ :param session: valid api session to ESX/VC server
+ :param host: ESX/VC server IP address[:port] or host name[:port]
+ :param vm_ref: managed object reference of the backing VM whose VMDK
+ is to be exported
+ :param vmdk_path: path of the VMDK file to be exported
+ :param vmdk_size: actual size of the VMDK file
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException
+ """
+ self._session = session
+ self._vmdk_size = vmdk_size
+ self._bytes_read = 0
+
+ # Obtain lease for VM export
+ self._lease = self._create_and_wait_for_lease(session, vm_ref)
+ LOG.debug(_("Invoking VIM API for reading info of lease: %s."),
+ self._lease)
+ lease_info = session.invoke_api(vim_util,
+ 'get_object_property',
+ session.vim,
+ self._lease,
+ 'info')
+
+ # find URL of the VMDK file to be read and open connection
+ self._url = self._find_vmdk_url(lease_info, host)
+ self._conn = self._create_connection(session, self._url)
+ FileHandle.__init__(self, self._conn)
+
+ def _create_and_wait_for_lease(self, session, vm_ref):
+ """Create and wait for HttpNfcLease lease for VM export."""
+ LOG.debug(_("Creating HttpNfcLease lease for exporting VM: %s."),
+ vm_ref)
+ lease = session.invoke_api(session.vim, 'ExportVm', vm_ref)
+ LOG.debug(_("Lease: %(lease)s obtained for exporting VM: %(vm_ref)s."),
+ {'lease': lease,
+ 'vm_ref': vm_ref})
+ session.wait_for_lease_ready(lease)
+ return lease
+
+ def _create_connection(self, session, url):
+ LOG.debug(_("Opening URL: %s for reading."), url)
+ try:
+ cookies = session.vim.client.options.transport.cookiejar
+ headers = {'User-Agent': USER_AGENT,
+ 'Cookie': self._build_vim_cookie_header(cookies)}
+ request = urllib2.Request(url, None, headers)
+ conn = urllib2.urlopen(request)
+ LOG.debug(_("URL: %s opened for reading."), url)
+ return conn
+ except Exception as excep:
+ # TODO(vbala) We need to catch and raise specific exceptions
+ # related to connection problems, invalid request and invalid
+ # arguments.
+ excep_msg = _("Error occurred while opening URL: %s for "
+ "reading.") % url
+ LOG.exception(excep_msg)
+ raise exceptions.VimException(excep_msg, excep)
+
+ def read(self, chunk_size):
+ """Read a chunk of data from the VMDK file.
+
+ :param chunk_size: size of read chunk
+ :returns: the data
+ :raises: VimException
+ """
+ LOG.debug(_("Reading data from VMDK file with URL = %s."), self._url)
+
+ try:
+ data = self._file_handle.read(READ_CHUNKSIZE)
+ self._bytes_read += len(data)
+ LOG.debug(_("Total %(bytes_read)d bytes read from VMDK file "
+ "with URL = %(url)s."),
+ {'bytes_read': self._bytes_read,
+ 'url': self._url})
+ return data
+ except Exception as excep:
+ # TODO(vbala) We need to catch and raise specific exceptions
+ # related to connection problems, invalid request and invalid
+ # arguments.
+ excep_msg = _("Error occurred while reading data from"
+ " %s.") % self._url
+ LOG.exception(excep_msg)
+ raise exceptions.VimException(excep_msg, excep)
+
+ def update_progress(self):
+ """Updates progress to lease.
+
+ This call back to the lease is essential to keep the lease alive
+ across long running read operations.
+
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException
+ """
+ percent = int(float(self._bytes_read) / self._vmdk_size * 100)
+ LOG.debug(_("Calling VIM API to update read progress of VMDK file"
+ " with URL = %(url)s to %(percent)d%%."),
+ {'url': self._url,
+ 'percent': percent})
+ try:
+ self._session.invoke_api(self._session.vim,
+ 'HttpNfcLeaseProgress',
+ self._lease,
+ percent=percent)
+ LOG.debug(_("Updated read progress of VMDK file with "
+ "URL = %(url)s to %(percent)d%%."),
+ {'url': self._url,
+ 'percent': percent})
+ except exceptions.VimException as excep:
+ LOG.exception(_("Error occurred while updating the read progress "
+ "of VMDK file with URL = %s."),
+ self._url)
+ raise excep
+
+ def close(self):
+ """Releases the lease and close the connection.
+
+ :raises: VimException, VimFaultException, VimAttributeException,
+ VimSessionOverLoadException, VimConnectionException
+ """
+ LOG.debug(_("Getting lease state for %s."), self._url)
+ try:
+ state = self._session.invoke_api(vim_util,
+ 'get_object_property',
+ self._session.vim,
+ self._lease,
+ 'state')
+ LOG.debug(_("Lease for %(url)s is in state: %(state)s."),
+ {'url': self._url,
+ 'state': state})
+ if state == 'ready':
+ LOG.debug(_("Releasing lease for %s."), self._url)
+ self._session.invoke_api(self._session.vim,
+ 'HttpNfcLeaseComplete',
+ self._lease)
+ LOG.debug(_("Lease for %s released."), self._url)
+ else:
+ LOG.debug(_("Lease for %(url)s is in state: %(state)s; no "
+ "need to release."),
+ {'url': self._url,
+ 'state': state})
+ except exceptions.VimException:
+ LOG.warn(_("Error occurred while releasing the lease for %s."),
+ self._url,
+ exc_info=True)
+ raise
+ super(VmdkReadHandle, self).close()
+ LOG.debug(_("Closed VMDK read handle for %s."), self._url)
+
+ def __str__(self):
+ return "VMDK read handle for %s" % self._url
+
+
+class ImageReadHandle(object):
+ """Read handle for glance images."""
+
+ def __init__(self, glance_read_iter):
+ """Initializes the read handle with given parameters.
+
+ :param glance_read_iter: iterator to read data from glance image
+ """
+ self._glance_read_iter = glance_read_iter
+ self._iter = self.get_next()
+
+ def read(self, chunk_size):
+ """Read an item from the image data iterator.
+
+ The input chunk size is ignored since the client ImageBodyIterator
+ uses its own chunk size.
+ """
+ try:
+ data = self._iter.next()
+ LOG.debug(_("Read %d bytes from the image iterator."), len(data))
+ return data
+ except StopIteration:
+ LOG.debug(_("Completed reading data from the image iterator."))
+ return ""
+
+ def get_next(self):
+ """Get the next item from the image iterator."""
+ for data in self._glance_read_iter:
+ yield data
+
+ def close(self):
+ """Close the read handle.
+
+ This is a NOP.
+ """
+ pass
+
+ def __str__(self):
+ return "Image read handle"
diff --git a/tests/test_rw_handles.py b/tests/test_rw_handles.py
new file mode 100644
index 0000000..a27f70d
--- /dev/null
+++ b/tests/test_rw_handles.py
@@ -0,0 +1,300 @@
+# Copyright (c) 2014 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.
+
+"""
+Unit tests for read and write handles for image transfer.
+"""
+
+import mock
+
+from oslo.vmware import exceptions
+from oslo.vmware import rw_handles
+from oslo.vmware import vim_util
+from tests import base
+
+
+class FileHandleTest(base.TestCase):
+ """Tests for FileHandle."""
+
+ def test_close(self):
+ file_handle = mock.Mock()
+ vmw_http_file = rw_handles.FileHandle(file_handle)
+ vmw_http_file.close()
+ file_handle.close.assert_called_once_with()
+
+ def test_del(self):
+ file_handle = mock.Mock()
+ vmw_http_file = rw_handles.FileHandle(file_handle)
+ del(vmw_http_file)
+ file_handle.close.assert_called_once_with()
+
+ def test_find_vmdk_url(self):
+ device_url_0 = mock.Mock()
+ device_url_0.disk = False
+ device_url_1 = mock.Mock()
+ device_url_1.disk = True
+ device_url_1.url = 'https://*/ds1/vm1.vmdk'
+ lease_info = mock.Mock()
+ lease_info.deviceUrl = [device_url_0, device_url_1]
+ host = '10.1.2.3'
+ exp_url = 'https://%s/ds1/vm1.vmdk' % host
+ vmw_http_file = rw_handles.FileHandle(None)
+ self.assertEqual(exp_url, vmw_http_file._find_vmdk_url(lease_info,
+ host))
+
+
+class FileWriteHandleTest(base.TestCase):
+ """Tests for FileWriteHandle."""
+
+ def setUp(self):
+ super(FileWriteHandleTest, self).setUp()
+
+ vim_cookie = mock.Mock()
+ vim_cookie.name = 'name'
+ vim_cookie.value = 'value'
+
+ self._conn = mock.Mock()
+ patcher = mock.patch('httplib.HTTPConnection')
+ self.addCleanup(patcher.stop)
+ HTTPConnectionMock = patcher.start()
+ HTTPConnectionMock.return_value = self._conn
+
+ self.vmw_http_write_file = rw_handles.FileWriteHandle(
+ '10.1.2.3', 'dc-0', 'ds-0', [vim_cookie], '1.vmdk', 100, 'http')
+
+ def test_write(self):
+ self.vmw_http_write_file.write(None)
+ self._conn.send.assert_called_once_with(None)
+
+ def test_close(self):
+ self.vmw_http_write_file.close()
+ self._conn.getresponse.assert_called_once_with()
+ self._conn.close.assert_called_once_with()
+
+
+class VmdkWriteHandleTest(base.TestCase):
+ """Tests for VmdkWriteHandle."""
+
+ def setUp(self):
+ super(VmdkWriteHandleTest, self).setUp()
+ self._conn = mock.Mock()
+ patcher = mock.patch('httplib.HTTPConnection')
+ self.addCleanup(patcher.stop)
+ HTTPConnectionMock = patcher.start()
+ HTTPConnectionMock.return_value = self._conn
+
+ def _create_mock_session(self, disk=True, progress=-1):
+ device_url = mock.Mock()
+ device_url.disk = disk
+ device_url.url = 'http://*/ds/disk1.vmdk'
+ lease_info = mock.Mock()
+ lease_info.deviceUrl = [device_url]
+ session = mock.Mock()
+
+ def session_invoke_api_side_effect(module, method, *args, **kwargs):
+ if module == session.vim:
+ if method == 'ImportVApp':
+ return mock.Mock()
+ elif method == 'HttpNfcLeaseProgress':
+ self.assertEqual(progress, kwargs['percent'])
+ return
+ return lease_info
+
+ session.invoke_api.side_effect = session_invoke_api_side_effect
+ vim_cookie = mock.Mock()
+ vim_cookie.name = 'name'
+ vim_cookie.value = 'value'
+ session.vim.client.options.transport.cookiejar = [vim_cookie]
+ return session
+
+ def test_init_failure(self):
+ session = self._create_mock_session(False)
+ self.assertRaises(exceptions.VimException,
+ lambda: rw_handles.VmdkWriteHandle(session,
+ '10.1.2.3',
+ 'rp-1',
+ 'folder-1',
+ None,
+ 100))
+
+ def test_write(self):
+ session = self._create_mock_session()
+ handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
+ 'rp-1', 'folder-1', None,
+ 100)
+ data = [1] * 10
+ handle.write(data)
+ self.assertEqual(len(data), handle._bytes_written)
+ self._conn.send.assert_called_once_with(data)
+
+ def test_update_progress(self):
+ vmdk_size = 100
+ data_size = 10
+ session = self._create_mock_session(True, 10)
+ handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
+ 'rp-1', 'folder-1', None,
+ vmdk_size)
+ handle.write([1] * data_size)
+ handle.update_progress()
+
+ def test_update_progress_with_error(self):
+ session = self._create_mock_session(True, 10)
+ handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
+ 'rp-1', 'folder-1', None,
+ 100)
+ session.invoke_api.side_effect = exceptions.VimException(None)
+ self.assertRaises(exceptions.VimException, handle.update_progress)
+
+ def test_close(self):
+ session = self._create_mock_session()
+ handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
+ 'rp-1', 'folder-1', None,
+ 100)
+
+ def session_invoke_api_side_effect(module, method, *args, **kwargs):
+ if module == vim_util and method == 'get_object_property':
+ return 'ready'
+ self.assertEqual(session.vim, module)
+ self.assertEqual('HttpNfcLeaseComplete', method)
+
+ session.invoke_api = mock.Mock(
+ side_effect=session_invoke_api_side_effect)
+ handle.close()
+ self.assertEqual(2, session.invoke_api.call_count)
+
+
+class VmdkReadHandleTest(base.TestCase):
+ """Tests for VmdkReadHandle."""
+
+ def setUp(self):
+ super(VmdkReadHandleTest, self).setUp()
+
+ req_patcher = mock.patch('urllib2.Request')
+ self.addCleanup(req_patcher.stop)
+ RequestMock = req_patcher.start()
+ RequestMock.return_value = mock.Mock()
+
+ urlopen_patcher = mock.patch('urllib2.urlopen')
+ self.addCleanup(urlopen_patcher.stop)
+ urlopen_mock = urlopen_patcher.start()
+ self._conn = mock.Mock()
+ urlopen_mock.return_value = self._conn
+
+ def _create_mock_session(self, disk=True, progress=-1):
+ device_url = mock.Mock()
+ device_url.disk = disk
+ device_url.url = 'http://*/ds/disk1.vmdk'
+ lease_info = mock.Mock()
+ lease_info.deviceUrl = [device_url]
+ session = mock.Mock()
+
+ def session_invoke_api_side_effect(module, method, *args, **kwargs):
+ if module == session.vim:
+ if method == 'ExportVm':
+ return mock.Mock()
+ elif method == 'HttpNfcLeaseProgress':
+ self.assertEqual(progress, kwargs['percent'])
+ return
+ return lease_info
+
+ session.invoke_api.side_effect = session_invoke_api_side_effect
+ vim_cookie = mock.Mock()
+ vim_cookie.name = 'name'
+ vim_cookie.value = 'value'
+ session.vim.client.options.transport.cookiejar = [vim_cookie]
+ return session
+
+ def test_init_failure(self):
+ session = self._create_mock_session(False)
+ self.assertRaises(exceptions.VimException,
+ lambda: rw_handles.VmdkReadHandle(session,
+ '10.1.2.3',
+ 'vm-1',
+ '[ds] disk1.vmdk',
+ 100))
+
+ def test_read(self):
+ chunk_size = rw_handles.READ_CHUNKSIZE
+ session = self._create_mock_session()
+ self._conn.read.return_value = [1] * chunk_size
+ handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
+ 'vm-1', '[ds] disk1.vmdk',
+ chunk_size * 10)
+ handle.read(chunk_size)
+ self.assertEqual(chunk_size, handle._bytes_read)
+ self._conn.read.assert_called_once_with(chunk_size)
+
+ def test_update_progress(self):
+ chunk_size = rw_handles.READ_CHUNKSIZE
+ vmdk_size = chunk_size * 10
+ session = self._create_mock_session(True, 10)
+ self._conn.read.return_value = [1] * chunk_size
+ handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
+ 'vm-1', '[ds] disk1.vmdk',
+ vmdk_size)
+ handle.read(chunk_size)
+ handle.update_progress()
+
+ def test_update_progress_with_error(self):
+ session = self._create_mock_session(True, 10)
+ handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
+ 'vm-1', '[ds] disk1.vmdk',
+ 100)
+ session.invoke_api.side_effect = exceptions.VimException(None)
+ self.assertRaises(exceptions.VimException, handle.update_progress)
+
+ def test_close(self):
+ session = self._create_mock_session()
+ handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
+ 'vm-1', '[ds] disk1.vmdk',
+ 100)
+
+ def session_invoke_api_side_effect(module, method, *args, **kwargs):
+ if module == vim_util and method == 'get_object_property':
+ return 'ready'
+ self.assertEqual(session.vim, module)
+ self.assertEqual('HttpNfcLeaseComplete', method)
+
+ session.invoke_api = mock.Mock(
+ side_effect=session_invoke_api_side_effect)
+ handle.close()
+ self.assertEqual(2, session.invoke_api.call_count)
+
+
+class ImageReadHandleTest(base.TestCase):
+ """Tests for ImageReadHandle."""
+
+ def test_read(self):
+ max_items = 10
+ item = [1] * 10
+
+ class ImageReadIterator:
+
+ def __init__(self):
+ self.num_items = 0
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if (self.num_items < max_items):
+ self.num_items += 1
+ return item
+ raise StopIteration
+
+ handle = rw_handles.ImageReadHandle(ImageReadIterator())
+ for _ in range(0, max_items):
+ self.assertEqual(item, handle.read(10))
+ self.assertFalse(handle.read(10))