Move files out of the namespace package
Move the public API out of oslo.vmware to oslo_vmware. Retain the ability to import from the old namespace package for backwards compatibility for this release cycle. bp/drop-namespace-packages Change-Id: I11cf038c3832a7357ed53363d8ccf143daddd2a2
This commit is contained in:
parent
bc6477ab79
commit
48771e6bfd
@ -3,4 +3,4 @@
|
|||||||
script=tools/run_cross_tests.sh
|
script=tools/run_cross_tests.sh
|
||||||
|
|
||||||
# The base module to hold the copy of openstack.common
|
# The base module to hold the copy of openstack.common
|
||||||
base=oslo.vmware
|
base=oslo_vmware
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
# 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 warnings
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated():
|
||||||
|
new_name = __name__.replace('.', '_')
|
||||||
|
warnings.warn(
|
||||||
|
('The oslo namespace package is deprecated. Please use %s instead.' %
|
||||||
|
new_name),
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
deprecated()
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,488 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.api import * # noqa
|
||||||
Session and API call management for VMware ESX/VC server.
|
|
||||||
|
|
||||||
This module contains classes to invoke VIM APIs. It supports
|
|
||||||
automatic session re-establishment and retry of API invocations
|
|
||||||
in case of connection problems or server API call overload.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo.utils import excutils
|
|
||||||
from oslo.vmware._i18n import _, _LE, _LI, _LW
|
|
||||||
from oslo.vmware.common import loopingcall
|
|
||||||
from oslo.vmware import exceptions
|
|
||||||
from oslo.vmware import pbm
|
|
||||||
from oslo.vmware import vim
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _trunc_id(session_id):
|
|
||||||
"""Returns truncated session id which is suitable for logging."""
|
|
||||||
if session_id is not None:
|
|
||||||
return session_id[-5:]
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(vbala) Move this class to excutils.py.
|
|
||||||
class RetryDecorator(object):
|
|
||||||
"""Decorator for retrying a function upon suggested exceptions.
|
|
||||||
|
|
||||||
The decorated function is retried for the given number of times, and the
|
|
||||||
sleep time between the retries is incremented until max sleep time is
|
|
||||||
reached. If the max retry count is set to -1, then the decorated function
|
|
||||||
is invoked indefinitely until an exception is thrown, and the caught
|
|
||||||
exception is not in the list of suggested exceptions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_retry_count=-1, inc_sleep_time=10,
|
|
||||||
max_sleep_time=60, exceptions=()):
|
|
||||||
"""Configure the retry object using the input params.
|
|
||||||
|
|
||||||
:param max_retry_count: maximum number of times the given function must
|
|
||||||
be retried when one of the input 'exceptions'
|
|
||||||
is caught. When set to -1, it will be retried
|
|
||||||
indefinitely until an exception is thrown
|
|
||||||
and the caught exception is not in param
|
|
||||||
exceptions.
|
|
||||||
:param inc_sleep_time: incremental time in seconds for sleep time
|
|
||||||
between retries
|
|
||||||
:param max_sleep_time: max sleep time in seconds beyond which the sleep
|
|
||||||
time will not be incremented using param
|
|
||||||
inc_sleep_time. On reaching this threshold,
|
|
||||||
max_sleep_time will be used as the sleep time.
|
|
||||||
:param exceptions: suggested exceptions for which the function must be
|
|
||||||
retried
|
|
||||||
"""
|
|
||||||
self._max_retry_count = max_retry_count
|
|
||||||
self._inc_sleep_time = inc_sleep_time
|
|
||||||
self._max_sleep_time = max_sleep_time
|
|
||||||
self._exceptions = exceptions
|
|
||||||
self._retry_count = 0
|
|
||||||
self._sleep_time = 0
|
|
||||||
|
|
||||||
def __call__(self, f):
|
|
||||||
|
|
||||||
def _func(*args, **kwargs):
|
|
||||||
func_name = f.__name__
|
|
||||||
result = None
|
|
||||||
try:
|
|
||||||
if self._retry_count:
|
|
||||||
LOG.debug("Invoking %(func_name)s; retry count is "
|
|
||||||
"%(retry_count)d.",
|
|
||||||
{'func_name': func_name,
|
|
||||||
'retry_count': self._retry_count})
|
|
||||||
result = f(*args, **kwargs)
|
|
||||||
except self._exceptions:
|
|
||||||
with excutils.save_and_reraise_exception() as ctxt:
|
|
||||||
LOG.warn(_LW("Exception which is in the suggested list of "
|
|
||||||
"exceptions occurred while invoking function:"
|
|
||||||
" %s."),
|
|
||||||
func_name,
|
|
||||||
exc_info=True)
|
|
||||||
if (self._max_retry_count != -1 and
|
|
||||||
self._retry_count >= self._max_retry_count):
|
|
||||||
LOG.error(_LE("Cannot retry upon suggested exception "
|
|
||||||
"since retry count (%(retry_count)d) "
|
|
||||||
"reached max retry count "
|
|
||||||
"(%(max_retry_count)d)."),
|
|
||||||
{'retry_count': self._retry_count,
|
|
||||||
'max_retry_count': self._max_retry_count})
|
|
||||||
else:
|
|
||||||
ctxt.reraise = False
|
|
||||||
self._retry_count += 1
|
|
||||||
self._sleep_time += self._inc_sleep_time
|
|
||||||
return self._sleep_time
|
|
||||||
raise loopingcall.LoopingCallDone(result)
|
|
||||||
|
|
||||||
def func(*args, **kwargs):
|
|
||||||
loop = loopingcall.DynamicLoopingCall(_func, *args, **kwargs)
|
|
||||||
evt = loop.start(periodic_interval_max=self._max_sleep_time)
|
|
||||||
LOG.debug("Waiting for function %s to return.", f.__name__)
|
|
||||||
return evt.wait()
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
class VMwareAPISession(object):
|
|
||||||
"""Setup a session with the server and handles all calls made to it.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
api_session = VMwareAPISession('10.1.2.3', 'administrator',
|
|
||||||
'password', 10, 0.1,
|
|
||||||
create_session=False, port=443)
|
|
||||||
result = api_session.invoke_api(vim_util, 'get_objects',
|
|
||||||
api_session.vim, 'HostSystem', 100)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, host, server_username, server_password,
|
|
||||||
api_retry_count, task_poll_interval, scheme='https',
|
|
||||||
create_session=True, wsdl_loc=None, pbm_wsdl_loc=None,
|
|
||||||
port=443, cacert=None, insecure=True):
|
|
||||||
"""Initializes the API session with given parameters.
|
|
||||||
|
|
||||||
:param host: ESX/VC server IP address or host name
|
|
||||||
:param port: port for connection
|
|
||||||
:param server_username: username of ESX/VC server admin user
|
|
||||||
:param server_password: password for param server_username
|
|
||||||
:param api_retry_count: number of times an API must be retried upon
|
|
||||||
session/connection related errors
|
|
||||||
:param task_poll_interval: sleep time in seconds for polling an
|
|
||||||
on-going async task as part of the API call
|
|
||||||
:param scheme: protocol-- http or https
|
|
||||||
:param create_session: whether to setup a connection at the time of
|
|
||||||
instance creation
|
|
||||||
:param wsdl_loc: VIM API WSDL file location
|
|
||||||
:param pbm_wsdl_loc: PBM service WSDL file location
|
|
||||||
:param cacert: Specify a CA bundle file to use in verifying a
|
|
||||||
TLS (https) server certificate.
|
|
||||||
:param insecure: Verify HTTPS connections using system certificates,
|
|
||||||
used only if cacert is not specified
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException
|
|
||||||
"""
|
|
||||||
self._host = host
|
|
||||||
self._port = port
|
|
||||||
self._server_username = server_username
|
|
||||||
self._server_password = server_password
|
|
||||||
self._api_retry_count = api_retry_count
|
|
||||||
self._task_poll_interval = task_poll_interval
|
|
||||||
self._scheme = scheme
|
|
||||||
self._vim_wsdl_loc = wsdl_loc
|
|
||||||
self._pbm_wsdl_loc = pbm_wsdl_loc
|
|
||||||
self._session_id = None
|
|
||||||
self._session_username = None
|
|
||||||
self._vim = None
|
|
||||||
self._pbm = None
|
|
||||||
self._cacert = cacert
|
|
||||||
self._insecure = insecure
|
|
||||||
if create_session:
|
|
||||||
self._create_session()
|
|
||||||
|
|
||||||
def pbm_wsdl_loc_set(self, pbm_wsdl_loc):
|
|
||||||
self._pbm_wsdl_loc = pbm_wsdl_loc
|
|
||||||
self._pbm = None
|
|
||||||
LOG.info(_LI('PBM WSDL updated to %s'), pbm_wsdl_loc)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vim(self):
|
|
||||||
if not self._vim:
|
|
||||||
self._vim = vim.Vim(protocol=self._scheme,
|
|
||||||
host=self._host,
|
|
||||||
port=self._port,
|
|
||||||
wsdl_url=self._vim_wsdl_loc,
|
|
||||||
cacert=self._cacert,
|
|
||||||
insecure=self._insecure)
|
|
||||||
return self._vim
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pbm(self):
|
|
||||||
if not self._pbm and self._pbm_wsdl_loc:
|
|
||||||
self._pbm = pbm.Pbm(protocol=self._scheme,
|
|
||||||
host=self._host,
|
|
||||||
port=self._port,
|
|
||||||
wsdl_url=self._pbm_wsdl_loc,
|
|
||||||
cacert=self._cacert,
|
|
||||||
insecure=self._insecure)
|
|
||||||
if self._session_id:
|
|
||||||
# To handle the case where pbm property is accessed after
|
|
||||||
# session creation. If pbm property is accessed before session
|
|
||||||
# creation, we set the cookie in _create_session.
|
|
||||||
self._pbm.set_soap_cookie(self._vim.get_http_cookie())
|
|
||||||
return self._pbm
|
|
||||||
|
|
||||||
@RetryDecorator(exceptions=(exceptions.VimConnectionException,))
|
|
||||||
def _create_session(self):
|
|
||||||
"""Establish session with the server."""
|
|
||||||
session_manager = self.vim.service_content.sessionManager
|
|
||||||
# Login and create new session with the server for making API calls.
|
|
||||||
LOG.debug("Logging in with username = %s.", self._server_username)
|
|
||||||
session = self.vim.Login(session_manager,
|
|
||||||
userName=self._server_username,
|
|
||||||
password=self._server_password)
|
|
||||||
prev_session_id, self._session_id = self._session_id, session.key
|
|
||||||
# We need to save the username in the session since we may need it
|
|
||||||
# later to check active session. The SessionIsActive method requires
|
|
||||||
# the username parameter to be exactly same as that in the session
|
|
||||||
# object. We can't use the username used for login since the Login
|
|
||||||
# method ignores the case.
|
|
||||||
self._session_username = session.userName
|
|
||||||
LOG.info(_LI("Successfully established new session; session ID is "
|
|
||||||
"%s."),
|
|
||||||
_trunc_id(self._session_id))
|
|
||||||
|
|
||||||
# Terminate the previous session (if exists) for preserving sessions
|
|
||||||
# as there is a limit on the number of sessions we can have.
|
|
||||||
if prev_session_id:
|
|
||||||
try:
|
|
||||||
LOG.info(_LI("Terminating the previous session with ID = %s"),
|
|
||||||
_trunc_id(prev_session_id))
|
|
||||||
self.vim.TerminateSession(session_manager,
|
|
||||||
sessionId=[prev_session_id])
|
|
||||||
except Exception:
|
|
||||||
# This exception is something we can live with. It is
|
|
||||||
# just an extra caution on our side. The session might
|
|
||||||
# have been cleared already. We could have made a call to
|
|
||||||
# SessionIsActive, but that is an overhead because we
|
|
||||||
# anyway would have to call TerminateSession.
|
|
||||||
LOG.warn(_LW("Error occurred while terminating the previous "
|
|
||||||
"session with ID = %s."),
|
|
||||||
_trunc_id(prev_session_id),
|
|
||||||
exc_info=True)
|
|
||||||
|
|
||||||
# Set PBM client cookie.
|
|
||||||
if self._pbm is not None:
|
|
||||||
self._pbm.set_soap_cookie(self._vim.get_http_cookie())
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
"""Log out and terminate the current session."""
|
|
||||||
if self._session_id:
|
|
||||||
LOG.info(_LI("Logging out and terminating the current session "
|
|
||||||
"with ID = %s."),
|
|
||||||
_trunc_id(self._session_id))
|
|
||||||
try:
|
|
||||||
self.vim.Logout(self.vim.service_content.sessionManager)
|
|
||||||
self._session_id = None
|
|
||||||
except Exception:
|
|
||||||
LOG.exception(_LE("Error occurred while logging out and "
|
|
||||||
"terminating the current session with "
|
|
||||||
"ID = %s."),
|
|
||||||
_trunc_id(self._session_id))
|
|
||||||
else:
|
|
||||||
LOG.debug("No session exists to log out.")
|
|
||||||
|
|
||||||
def invoke_api(self, module, method, *args, **kwargs):
|
|
||||||
"""Wrapper method for invoking APIs.
|
|
||||||
|
|
||||||
The API call is retried in the event of exceptions due to session
|
|
||||||
overload or connection problems.
|
|
||||||
|
|
||||||
:param module: module corresponding to the VIM API call
|
|
||||||
:param method: method in the module which corresponds to the
|
|
||||||
VIM API call
|
|
||||||
:param args: arguments to the method
|
|
||||||
:param kwargs: keyword arguments to the method
|
|
||||||
:returns: response from the API call
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
|
|
||||||
@RetryDecorator(max_retry_count=self._api_retry_count,
|
|
||||||
exceptions=(exceptions.VimSessionOverLoadException,
|
|
||||||
exceptions.VimConnectionException))
|
|
||||||
def _invoke_api(module, method, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
api_method = getattr(module, method)
|
|
||||||
return api_method(*args, **kwargs)
|
|
||||||
except exceptions.VimFaultException as excep:
|
|
||||||
# If this is due to an inactive session, we should re-create
|
|
||||||
# the session and retry.
|
|
||||||
if exceptions.NOT_AUTHENTICATED in excep.fault_list:
|
|
||||||
# The NotAuthenticated fault is set by the fault checker
|
|
||||||
# due to an empty response. An empty response could be a
|
|
||||||
# valid response; for e.g., response for the query to
|
|
||||||
# return the VMs in an ESX server which has no VMs in it.
|
|
||||||
# Also, the server responds with an empty response in the
|
|
||||||
# case of an inactive session. Therefore, we need a way to
|
|
||||||
# differentiate between these two cases.
|
|
||||||
if self.is_current_session_active():
|
|
||||||
LOG.debug("Returning empty response for "
|
|
||||||
"%(module)s.%(method)s invocation.",
|
|
||||||
{'module': module,
|
|
||||||
'method': method})
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
# empty response is due to an inactive session
|
|
||||||
excep_msg = (
|
|
||||||
_("Current session: %(session)s is inactive; "
|
|
||||||
"re-creating the session while invoking "
|
|
||||||
"method %(module)s.%(method)s.") %
|
|
||||||
{'session': _trunc_id(self._session_id),
|
|
||||||
'module': module,
|
|
||||||
'method': method})
|
|
||||||
LOG.warn(excep_msg, exc_info=True)
|
|
||||||
self._create_session()
|
|
||||||
raise exceptions.VimConnectionException(excep_msg,
|
|
||||||
excep)
|
|
||||||
else:
|
|
||||||
# no need to retry for other VIM faults like
|
|
||||||
# InvalidArgument
|
|
||||||
# Raise specific exceptions here if possible
|
|
||||||
if excep.fault_list:
|
|
||||||
LOG.debug("Fault list: %s", excep.fault_list)
|
|
||||||
fault = excep.fault_list[0]
|
|
||||||
clazz = exceptions.get_fault_class(fault)
|
|
||||||
raise clazz(six.text_type(excep), excep.details)
|
|
||||||
raise
|
|
||||||
|
|
||||||
except exceptions.VimConnectionException:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
# Re-create the session during connection exception only
|
|
||||||
# if the session has expired. Otherwise, it could be
|
|
||||||
# a transient issue.
|
|
||||||
if not self.is_current_session_active():
|
|
||||||
LOG.warn(_LW("Re-creating session due to connection "
|
|
||||||
"problems while invoking method "
|
|
||||||
"%(module)s.%(method)s."),
|
|
||||||
{'module': module,
|
|
||||||
'method': method},
|
|
||||||
exc_info=True)
|
|
||||||
self._create_session()
|
|
||||||
|
|
||||||
return _invoke_api(module, method, *args, **kwargs)
|
|
||||||
|
|
||||||
def is_current_session_active(self):
|
|
||||||
"""Check if current session is active.
|
|
||||||
|
|
||||||
:returns: True if the session is active; False otherwise
|
|
||||||
"""
|
|
||||||
LOG.debug("Checking if the current session: %s is active.",
|
|
||||||
_trunc_id(self._session_id))
|
|
||||||
|
|
||||||
is_active = False
|
|
||||||
try:
|
|
||||||
is_active = self.vim.SessionIsActive(
|
|
||||||
self.vim.service_content.sessionManager,
|
|
||||||
sessionID=self._session_id,
|
|
||||||
userName=self._session_username)
|
|
||||||
except exceptions.VimException:
|
|
||||||
LOG.warn(_LW("Error occurred while checking whether the "
|
|
||||||
"current session: %s is active."),
|
|
||||||
_trunc_id(self._session_id),
|
|
||||||
exc_info=True)
|
|
||||||
|
|
||||||
return is_active
|
|
||||||
|
|
||||||
def wait_for_task(self, task):
|
|
||||||
"""Waits for the given task to complete and returns the result.
|
|
||||||
|
|
||||||
The task is polled until it is done. The method returns the task
|
|
||||||
information upon successful completion. In case of any error,
|
|
||||||
appropriate exception is raised.
|
|
||||||
|
|
||||||
:param task: managed object reference of the task
|
|
||||||
:returns: task info upon successful completion of the task
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
loop = loopingcall.FixedIntervalLoopingCall(self._poll_task, task)
|
|
||||||
evt = loop.start(self._task_poll_interval)
|
|
||||||
LOG.debug("Waiting for the task: %s to complete.", task)
|
|
||||||
return evt.wait()
|
|
||||||
|
|
||||||
def _poll_task(self, task):
|
|
||||||
"""Poll the given task until completion.
|
|
||||||
|
|
||||||
If the task completes successfully, the method returns the task info
|
|
||||||
using the input event (param done). In case of any error, appropriate
|
|
||||||
exception is set in the event.
|
|
||||||
|
|
||||||
:param task: managed object reference of the task
|
|
||||||
"""
|
|
||||||
LOG.debug("Invoking VIM API to read info of task: %s.", task)
|
|
||||||
try:
|
|
||||||
task_info = self.invoke_api(vim_util,
|
|
||||||
'get_object_property',
|
|
||||||
self.vim,
|
|
||||||
task,
|
|
||||||
'info')
|
|
||||||
except exceptions.VimException:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.exception(_LE("Error occurred while reading info of "
|
|
||||||
"task: %s."),
|
|
||||||
task)
|
|
||||||
else:
|
|
||||||
if task_info.state in ['queued', 'running']:
|
|
||||||
if hasattr(task_info, 'progress'):
|
|
||||||
LOG.debug("Task: %(task)s progress is %(progress)s%%.",
|
|
||||||
{'task': task,
|
|
||||||
'progress': task_info.progress})
|
|
||||||
elif task_info.state == 'success':
|
|
||||||
LOG.debug("Task: %s status is success.", task)
|
|
||||||
raise loopingcall.LoopingCallDone(task_info)
|
|
||||||
else:
|
|
||||||
error_msg = six.text_type(task_info.error.localizedMessage)
|
|
||||||
error = task_info.error
|
|
||||||
name = error.fault.__class__.__name__
|
|
||||||
task_ex = exceptions.get_fault_class(name)(error_msg)
|
|
||||||
raise task_ex
|
|
||||||
|
|
||||||
def wait_for_lease_ready(self, lease):
|
|
||||||
"""Waits for the given lease to be ready.
|
|
||||||
|
|
||||||
This method return when the lease is ready. In case of any error,
|
|
||||||
appropriate exception is raised.
|
|
||||||
|
|
||||||
:param lease: lease to be checked for
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
loop = loopingcall.FixedIntervalLoopingCall(self._poll_lease, lease)
|
|
||||||
evt = loop.start(self._task_poll_interval)
|
|
||||||
LOG.debug("Waiting for the lease: %s to be ready.", lease)
|
|
||||||
evt.wait()
|
|
||||||
|
|
||||||
def _poll_lease(self, lease):
|
|
||||||
"""Poll the state of the given lease.
|
|
||||||
|
|
||||||
When the lease is ready, the event (param done) is notified. In case
|
|
||||||
of any error, appropriate exception is set in the event.
|
|
||||||
|
|
||||||
:param lease: lease whose state is to be polled
|
|
||||||
"""
|
|
||||||
LOG.debug("Invoking VIM API to read state of lease: %s.", lease)
|
|
||||||
try:
|
|
||||||
state = self.invoke_api(vim_util,
|
|
||||||
'get_object_property',
|
|
||||||
self.vim,
|
|
||||||
lease,
|
|
||||||
'state')
|
|
||||||
except exceptions.VimException:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.exception(_LE("Error occurred while checking "
|
|
||||||
"state of lease: %s."),
|
|
||||||
lease)
|
|
||||||
else:
|
|
||||||
if state == 'ready':
|
|
||||||
LOG.debug("Lease: %s is ready.", lease)
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
elif state == 'initializing':
|
|
||||||
LOG.debug("Lease: %s is initializing.", lease)
|
|
||||||
elif state == 'error':
|
|
||||||
LOG.debug("Invoking VIM API to read lease: %s error.",
|
|
||||||
lease)
|
|
||||||
error_msg = self._get_error_message(lease)
|
|
||||||
excep_msg = _("Lease: %(lease)s is in error state. Details: "
|
|
||||||
"%(error_msg)s.") % {'lease': lease,
|
|
||||||
'error_msg': error_msg}
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
raise exceptions.VimException(excep_msg)
|
|
||||||
else:
|
|
||||||
# unknown state
|
|
||||||
excep_msg = _("Unknown state: %(state)s for lease: "
|
|
||||||
"%(lease)s.") % {'state': state,
|
|
||||||
'lease': lease}
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
raise exceptions.VimException(excep_msg)
|
|
||||||
|
|
||||||
def _get_error_message(self, lease):
|
|
||||||
"""Get error message associated with the given lease."""
|
|
||||||
try:
|
|
||||||
return self.invoke_api(vim_util,
|
|
||||||
'get_object_property',
|
|
||||||
self.vim,
|
|
||||||
lease,
|
|
||||||
'error')
|
|
||||||
except exceptions.VimException:
|
|
||||||
LOG.warn(_LW("Error occurred while reading error message for "
|
|
||||||
"lease: %s."),
|
|
||||||
lease,
|
|
||||||
exc_info=True)
|
|
||||||
return "Unknown"
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,20 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_vmware.constants import * # noqa
|
||||||
"""
|
|
||||||
Shared constants across the VMware ecosystem.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Datacenter path for HTTP access to datastores if the target server is an ESX/
|
|
||||||
# ESXi system: http://goo.gl/B5Htr8 for more information.
|
|
||||||
ESX_DATACENTER_PATH = 'ha-datacenter'
|
|
||||||
|
|
||||||
# User Agent for HTTP requests between OpenStack and vCenter.
|
|
||||||
USER_AGENT = 'OpenStack-ESX-Adapter'
|
|
||||||
|
|
||||||
# Key of the cookie header when using a SOAP session.
|
|
||||||
SOAP_COOKIE_KEY = 'vmware_soap_session'
|
|
||||||
|
|
||||||
# Key of the cookie header when using a CGI session.
|
|
||||||
CGI_COOKIE_KEY = 'vmware_cgi_ticket'
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,249 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.exceptions import * # noqa
|
||||||
Exception definitions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo.vmware._i18n import _, _LE
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ALREADY_EXISTS = 'AlreadyExists'
|
|
||||||
CANNOT_DELETE_FILE = 'CannotDeleteFile'
|
|
||||||
FILE_ALREADY_EXISTS = 'FileAlreadyExists'
|
|
||||||
FILE_FAULT = 'FileFault'
|
|
||||||
FILE_LOCKED = 'FileLocked'
|
|
||||||
FILE_NOT_FOUND = 'FileNotFound'
|
|
||||||
INVALID_POWER_STATE = 'InvalidPowerState'
|
|
||||||
INVALID_PROPERTY = 'InvalidProperty'
|
|
||||||
NO_PERMISSION = 'NoPermission'
|
|
||||||
NOT_AUTHENTICATED = 'NotAuthenticated'
|
|
||||||
TASK_IN_PROGRESS = 'TaskInProgress'
|
|
||||||
DUPLICATE_NAME = 'DuplicateName'
|
|
||||||
|
|
||||||
|
|
||||||
class VimException(Exception):
|
|
||||||
"""The base exception class for all exceptions this library raises."""
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
__str__ = lambda self: six.text_type(self).encode('utf8')
|
|
||||||
__unicode__ = lambda self: self.description
|
|
||||||
else:
|
|
||||||
__str__ = lambda self: self.description
|
|
||||||
|
|
||||||
def __init__(self, message, cause=None):
|
|
||||||
Exception.__init__(self)
|
|
||||||
if isinstance(message, list):
|
|
||||||
# we need this to protect against developers using
|
|
||||||
# this method like VimFaultException
|
|
||||||
raise ValueError(_("exception_summary must not be a list"))
|
|
||||||
|
|
||||||
self.msg = message
|
|
||||||
self.cause = cause
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
# NOTE(jecarey): self.msg and self.cause may be i18n objects
|
|
||||||
# that do not support str or concatenation, but can be used
|
|
||||||
# as replacement text.
|
|
||||||
descr = six.text_type(self.msg)
|
|
||||||
if self.cause:
|
|
||||||
descr += '\nCause: ' + six.text_type(self.cause)
|
|
||||||
return descr
|
|
||||||
|
|
||||||
|
|
||||||
class VimSessionOverLoadException(VimException):
|
|
||||||
"""Thrown when there is an API call overload at the VMware server."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VimConnectionException(VimException):
|
|
||||||
"""Thrown when there is a connection problem."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VimAttributeException(VimException):
|
|
||||||
"""Thrown when a particular attribute cannot be found."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VimFaultException(VimException):
|
|
||||||
"""Exception thrown when there are faults during VIM API calls."""
|
|
||||||
|
|
||||||
def __init__(self, fault_list, message, cause=None, details=None):
|
|
||||||
super(VimFaultException, self).__init__(message, cause)
|
|
||||||
if not isinstance(fault_list, list):
|
|
||||||
raise ValueError(_("fault_list must be a list"))
|
|
||||||
if details is not None and not isinstance(details, dict):
|
|
||||||
raise ValueError(_("details must be a dict"))
|
|
||||||
self.fault_list = fault_list
|
|
||||||
self.details = details
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
__unicode__ = lambda self: self.description
|
|
||||||
else:
|
|
||||||
__str__ = lambda self: self.description
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
descr = VimException.description.fget(self)
|
|
||||||
if self.fault_list:
|
|
||||||
# fault_list doesn't contain non-ASCII chars, we can use str()
|
|
||||||
descr += '\nFaults: ' + str(self.fault_list)
|
|
||||||
if self.details:
|
|
||||||
# details may contain non-ASCII values
|
|
||||||
details = '{%s}' % ', '.join(["'%s': '%s'" % (k, v) for k, v in
|
|
||||||
six.iteritems(self.details)])
|
|
||||||
descr += '\nDetails: ' + details
|
|
||||||
return descr
|
|
||||||
|
|
||||||
|
|
||||||
class ImageTransferException(VimException):
|
|
||||||
"""Thrown when there is an error during image transfer."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VMwareDriverException(Exception):
|
|
||||||
"""Base VMware Driver Exception
|
|
||||||
|
|
||||||
To correctly use this class, inherit from it and define
|
|
||||||
a 'msg_fmt' property. That msg_fmt will get printf'd
|
|
||||||
with the keyword arguments provided to the constructor.
|
|
||||||
|
|
||||||
"""
|
|
||||||
msg_fmt = _("An unknown exception occurred.")
|
|
||||||
|
|
||||||
def __init__(self, message=None, details=None, **kwargs):
|
|
||||||
self.kwargs = kwargs
|
|
||||||
self.details = details
|
|
||||||
|
|
||||||
if not message:
|
|
||||||
try:
|
|
||||||
message = self.msg_fmt % kwargs
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# kwargs doesn't match a variable in the message
|
|
||||||
# log the issue and the kwargs
|
|
||||||
LOG.exception(_LE('Exception in string format operation'))
|
|
||||||
for name, value in six.iteritems(kwargs):
|
|
||||||
LOG.error(_LE("%(name)s: %(value)s"),
|
|
||||||
{'name': name, 'value': value})
|
|
||||||
# at least get the core message out if something happened
|
|
||||||
message = self.msg_fmt
|
|
||||||
|
|
||||||
super(VMwareDriverException, self).__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
class VMwareDriverConfigurationException(VMwareDriverException):
|
|
||||||
"""Base class for all configuration exceptions.
|
|
||||||
"""
|
|
||||||
msg_fmt = _("VMware Driver configuration fault.")
|
|
||||||
|
|
||||||
|
|
||||||
class UseLinkedCloneConfigurationFault(VMwareDriverConfigurationException):
|
|
||||||
msg_fmt = _("No default value for use_linked_clone found.")
|
|
||||||
|
|
||||||
|
|
||||||
class MissingParameter(VMwareDriverException):
|
|
||||||
msg_fmt = _("Missing parameter : %(param)s")
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExistsException(VMwareDriverException):
|
|
||||||
msg_fmt = _("Resource already exists.")
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class CannotDeleteFileException(VMwareDriverException):
|
|
||||||
msg_fmt = _("Cannot delete file.")
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class FileAlreadyExistsException(VMwareDriverException):
|
|
||||||
msg_fmt = _("File already exists.")
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class FileFaultException(VMwareDriverException):
|
|
||||||
msg_fmt = _("File fault.")
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class FileLockedException(VMwareDriverException):
|
|
||||||
msg_fmt = _("File locked.")
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class FileNotFoundException(VMwareDriverException):
|
|
||||||
msg_fmt = _("File not found.")
|
|
||||||
code = 404
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPowerStateException(VMwareDriverException):
|
|
||||||
msg_fmt = _("Invalid power state.")
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPropertyException(VMwareDriverException):
|
|
||||||
msg_fmt = _("Invalid property.")
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class NoPermissionException(VMwareDriverException):
|
|
||||||
msg_fmt = _("No Permission.")
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class NotAuthenticatedException(VMwareDriverException):
|
|
||||||
msg_fmt = _("Not Authenticated.")
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class TaskInProgress(VMwareDriverException):
|
|
||||||
msg_fmt = _("Entity has another operation in process.")
|
|
||||||
|
|
||||||
|
|
||||||
class DuplicateName(VMwareDriverException):
|
|
||||||
msg_fmt = _("Duplicate name.")
|
|
||||||
|
|
||||||
|
|
||||||
# Populate the fault registry with the exceptions that have
|
|
||||||
# special treatment.
|
|
||||||
_fault_classes_registry = {
|
|
||||||
ALREADY_EXISTS: AlreadyExistsException,
|
|
||||||
CANNOT_DELETE_FILE: CannotDeleteFileException,
|
|
||||||
FILE_ALREADY_EXISTS: FileAlreadyExistsException,
|
|
||||||
FILE_FAULT: FileFaultException,
|
|
||||||
FILE_LOCKED: FileLockedException,
|
|
||||||
FILE_NOT_FOUND: FileNotFoundException,
|
|
||||||
INVALID_POWER_STATE: InvalidPowerStateException,
|
|
||||||
INVALID_PROPERTY: InvalidPropertyException,
|
|
||||||
NO_PERMISSION: NoPermissionException,
|
|
||||||
NOT_AUTHENTICATED: NotAuthenticatedException,
|
|
||||||
TASK_IN_PROGRESS: TaskInProgress,
|
|
||||||
DUPLICATE_NAME: DuplicateName,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_fault_class(name):
|
|
||||||
"""Get a named subclass of VMwareDriverException."""
|
|
||||||
name = str(name)
|
|
||||||
fault_class = _fault_classes_registry.get(name)
|
|
||||||
if not fault_class:
|
|
||||||
LOG.debug('Fault %s not matched.', name)
|
|
||||||
fault_class = VMwareDriverException
|
|
||||||
return fault_class
|
|
||||||
|
|
||||||
|
|
||||||
def register_fault_class(name, exception):
|
|
||||||
fault_class = _fault_classes_registry.get(name)
|
|
||||||
if not issubclass(exception, VMwareDriverException):
|
|
||||||
raise TypeError(_("exception should be a subclass of "
|
|
||||||
"VMwareDriverException"))
|
|
||||||
if fault_class:
|
|
||||||
LOG.debug('Overriding exception for %s', name)
|
|
||||||
_fault_classes_registry[name] = exception
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,596 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.image_transfer import * # noqa
|
||||||
Functions and classes for image transfer between ESX/VC & image service.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from eventlet import event
|
|
||||||
from eventlet import greenthread
|
|
||||||
from eventlet import queue
|
|
||||||
from eventlet import timeout
|
|
||||||
|
|
||||||
from oslo.vmware._i18n import _
|
|
||||||
from oslo.vmware import constants
|
|
||||||
from oslo.vmware import exceptions
|
|
||||||
from oslo.vmware.objects import datastore as ds_obj
|
|
||||||
from oslo.vmware import rw_handles
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
IMAGE_SERVICE_POLL_INTERVAL = 5
|
|
||||||
FILE_READ_WRITE_TASK_SLEEP_TIME = 0.01
|
|
||||||
BLOCKING_QUEUE_SIZE = 10
|
|
||||||
|
|
||||||
|
|
||||||
class BlockingQueue(queue.LightQueue):
|
|
||||||
"""Producer-Consumer queue to share data between reader/writer threads."""
|
|
||||||
|
|
||||||
def __init__(self, max_size, max_transfer_size):
|
|
||||||
"""Initializes the queue with the given parameters.
|
|
||||||
|
|
||||||
:param max_size: maximum queue size; if max_size is less than zero or
|
|
||||||
None, the queue size is infinite.
|
|
||||||
:param max_transfer_size: maximum amount of data that can be
|
|
||||||
_transferred using this queue
|
|
||||||
"""
|
|
||||||
queue.LightQueue.__init__(self, max_size)
|
|
||||||
self._max_transfer_size = max_transfer_size
|
|
||||||
self._transferred = 0
|
|
||||||
|
|
||||||
def read(self, chunk_size):
|
|
||||||
"""Read data from the queue.
|
|
||||||
|
|
||||||
This method blocks until data is available. The input chunk size is
|
|
||||||
ignored since we have ensured that the data chunks written to the pipe
|
|
||||||
by the image reader thread is the same as the chunks asked for by the
|
|
||||||
image writer thread.
|
|
||||||
"""
|
|
||||||
if (self._max_transfer_size is 0 or
|
|
||||||
self._transferred < self._max_transfer_size):
|
|
||||||
data_item = self.get()
|
|
||||||
self._transferred += len(data_item)
|
|
||||||
return data_item
|
|
||||||
else:
|
|
||||||
LOG.debug("Completed transfer of size %s.", self._transferred)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
"""Write data into the queue.
|
|
||||||
|
|
||||||
:param data: data to be written
|
|
||||||
"""
|
|
||||||
self.put(data)
|
|
||||||
|
|
||||||
# Below methods are provided in order to enable treating the queue
|
|
||||||
# as a file handle.
|
|
||||||
|
|
||||||
def seek(self, offset, whence=0):
|
|
||||||
"""Set the file's current position at the offset.
|
|
||||||
|
|
||||||
This method throws IOError since seek cannot be supported for a pipe.
|
|
||||||
"""
|
|
||||||
raise IOError(errno.ESPIPE, "Illegal seek")
|
|
||||||
|
|
||||||
def tell(self):
|
|
||||||
"""Get the current file position."""
|
|
||||||
return self._transferred
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "blocking queue"
|
|
||||||
|
|
||||||
|
|
||||||
class ImageWriter(object):
|
|
||||||
"""Class to write the image to the image service from an input file."""
|
|
||||||
|
|
||||||
def __init__(self, context, input_file, image_service, image_id,
|
|
||||||
image_meta=None):
|
|
||||||
"""Initializes the image writer instance with given parameters.
|
|
||||||
|
|
||||||
:param context: write context needed by the image service
|
|
||||||
:param input_file: file to read the image data from
|
|
||||||
:param image_service: handle to image service
|
|
||||||
:param image_id: ID of the image in the image service
|
|
||||||
:param image_meta: image meta-data
|
|
||||||
"""
|
|
||||||
if not image_meta:
|
|
||||||
image_meta = {}
|
|
||||||
|
|
||||||
self._context = context
|
|
||||||
self._input_file = input_file
|
|
||||||
self._image_service = image_service
|
|
||||||
self._image_id = image_id
|
|
||||||
self._image_meta = image_meta
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Start the image write task.
|
|
||||||
|
|
||||||
:returns: the event indicating the status of the write task
|
|
||||||
"""
|
|
||||||
self._done = event.Event()
|
|
||||||
|
|
||||||
def _inner():
|
|
||||||
"""Task performing the image write operation.
|
|
||||||
|
|
||||||
This method performs image data transfer through an update call.
|
|
||||||
After the update, it waits until the image state becomes
|
|
||||||
'active', 'killed' or unknown. If the final state is not 'active'
|
|
||||||
an instance of ImageTransferException is thrown.
|
|
||||||
|
|
||||||
:raises: ImageTransferException
|
|
||||||
"""
|
|
||||||
LOG.debug("Calling image service update on image: %(image)s "
|
|
||||||
"with meta: %(meta)s",
|
|
||||||
{'image': self._image_id,
|
|
||||||
'meta': self._image_meta})
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._image_service.update(self._context,
|
|
||||||
self._image_id,
|
|
||||||
self._image_meta,
|
|
||||||
data=self._input_file)
|
|
||||||
self._running = True
|
|
||||||
while self._running:
|
|
||||||
LOG.debug("Retrieving status of image: %s.",
|
|
||||||
self._image_id)
|
|
||||||
image_meta = self._image_service.show(self._context,
|
|
||||||
self._image_id)
|
|
||||||
image_status = image_meta.get('status')
|
|
||||||
if image_status == 'active':
|
|
||||||
self.stop()
|
|
||||||
LOG.debug("Image: %s is now active.",
|
|
||||||
self._image_id)
|
|
||||||
self._done.send(True)
|
|
||||||
elif image_status == 'killed':
|
|
||||||
self.stop()
|
|
||||||
excep_msg = (_("Image: %s is in killed state.") %
|
|
||||||
self._image_id)
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
excep = exceptions.ImageTransferException(excep_msg)
|
|
||||||
self._done.send_exception(excep)
|
|
||||||
elif image_status in ['saving', 'queued']:
|
|
||||||
LOG.debug("Image: %(image)s is in %(state)s state; "
|
|
||||||
"sleeping for %(sleep)d seconds.",
|
|
||||||
{'image': self._image_id,
|
|
||||||
'state': image_status,
|
|
||||||
'sleep': IMAGE_SERVICE_POLL_INTERVAL})
|
|
||||||
greenthread.sleep(IMAGE_SERVICE_POLL_INTERVAL)
|
|
||||||
else:
|
|
||||||
self.stop()
|
|
||||||
excep_msg = (_("Image: %(image)s is in unknown "
|
|
||||||
"state: %(state)s.") %
|
|
||||||
{'image': self._image_id,
|
|
||||||
'state': image_status})
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
excep = exceptions.ImageTransferException(excep_msg)
|
|
||||||
self._done.send_exception(excep)
|
|
||||||
except Exception as excep:
|
|
||||||
self.stop()
|
|
||||||
excep_msg = (_("Error occurred while writing image: %s") %
|
|
||||||
self._image_id)
|
|
||||||
LOG.exception(excep_msg)
|
|
||||||
excep = exceptions.ImageTransferException(excep_msg, excep)
|
|
||||||
self._done.send_exception(excep)
|
|
||||||
|
|
||||||
LOG.debug("Starting image write task for image: %(image)s with"
|
|
||||||
" source: %(source)s.",
|
|
||||||
{'source': self._input_file,
|
|
||||||
'image': self._image_id})
|
|
||||||
greenthread.spawn(_inner)
|
|
||||||
return self._done
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Stop the image writing task."""
|
|
||||||
LOG.debug("Stopping the writing task for image: %s.",
|
|
||||||
self._image_id)
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
"""Wait for the image writer task to complete.
|
|
||||||
|
|
||||||
This method returns True if the writer thread completes successfully.
|
|
||||||
In case of error, it raises ImageTransferException.
|
|
||||||
|
|
||||||
:raises ImageTransferException
|
|
||||||
"""
|
|
||||||
return self._done.wait()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""This is a NOP."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = "Image Writer <source = %s, dest = %s>" % (self._input_file,
|
|
||||||
self._image_id)
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
class FileReadWriteTask(object):
|
|
||||||
"""Task which reads data from the input file and writes to the output file.
|
|
||||||
|
|
||||||
This class defines the task which copies the given input file to the given
|
|
||||||
output file. The copy operation involves reading chunks of data from the
|
|
||||||
input file and writing the same to the output file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, input_file, output_file):
|
|
||||||
"""Initializes the read-write task with the given input parameters.
|
|
||||||
|
|
||||||
:param input_file: the input file handle
|
|
||||||
:param output_file: the output file handle
|
|
||||||
"""
|
|
||||||
self._input_file = input_file
|
|
||||||
self._output_file = output_file
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Start the file read - file write task.
|
|
||||||
|
|
||||||
:returns: the event indicating the status of the read-write task
|
|
||||||
"""
|
|
||||||
self._done = event.Event()
|
|
||||||
|
|
||||||
def _inner():
|
|
||||||
"""Task performing the file read-write operation."""
|
|
||||||
self._running = True
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
data = self._input_file.read(rw_handles.READ_CHUNKSIZE)
|
|
||||||
if not data:
|
|
||||||
LOG.debug("File read-write task is done.")
|
|
||||||
self.stop()
|
|
||||||
self._done.send(True)
|
|
||||||
self._output_file.write(data)
|
|
||||||
|
|
||||||
# update lease progress if applicable
|
|
||||||
if hasattr(self._input_file, "update_progress"):
|
|
||||||
self._input_file.update_progress()
|
|
||||||
if hasattr(self._output_file, "update_progress"):
|
|
||||||
self._output_file.update_progress()
|
|
||||||
|
|
||||||
greenthread.sleep(FILE_READ_WRITE_TASK_SLEEP_TIME)
|
|
||||||
except Exception as excep:
|
|
||||||
self.stop()
|
|
||||||
excep_msg = _("Error occurred during file read-write "
|
|
||||||
"task.")
|
|
||||||
LOG.exception(excep_msg)
|
|
||||||
excep = exceptions.ImageTransferException(excep_msg, excep)
|
|
||||||
self._done.send_exception(excep)
|
|
||||||
|
|
||||||
LOG.debug("Starting file read-write task with source: %(source)s "
|
|
||||||
"and destination: %(dest)s.",
|
|
||||||
{'source': self._input_file,
|
|
||||||
'dest': self._output_file})
|
|
||||||
greenthread.spawn(_inner)
|
|
||||||
return self._done
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Stop the read-write task."""
|
|
||||||
LOG.debug("Stopping the file read-write task.")
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
"""Wait for the file read-write task to complete.
|
|
||||||
|
|
||||||
This method returns True if the read-write thread completes
|
|
||||||
successfully. In case of error, it raises ImageTransferException.
|
|
||||||
|
|
||||||
:raises: ImageTransferException
|
|
||||||
"""
|
|
||||||
return self._done.wait()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = ("File Read-Write Task <source = %s, dest = %s>" %
|
|
||||||
(self._input_file, self._output_file))
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
# Functions to perform image transfer between VMware servers and image service.
|
|
||||||
|
|
||||||
|
|
||||||
def _start_transfer(context, timeout_secs, read_file_handle, max_data_size,
|
|
||||||
write_file_handle=None, image_service=None, image_id=None,
|
|
||||||
image_meta=None):
|
|
||||||
"""Start the image transfer.
|
|
||||||
|
|
||||||
The image reader reads the data from the image source and writes to the
|
|
||||||
blocking queue. The image source is always a file handle (VmdkReadHandle
|
|
||||||
or ImageReadHandle); therefore, a FileReadWriteTask is created for this
|
|
||||||
transfer. The image writer reads the data from the blocking queue and
|
|
||||||
writes it to the image destination. The image destination is either a
|
|
||||||
file or VMDK in VMware datastore or an image in the image service.
|
|
||||||
|
|
||||||
If the destination is a file or VMDK in VMware datastore, the method
|
|
||||||
creates a FileReadWriteTask which reads from the blocking queue and
|
|
||||||
writes to either FileWriteHandle or VmdkWriteHandle. In the case of
|
|
||||||
image service as the destination, an instance of ImageWriter task is
|
|
||||||
created which reads from the blocking queue and writes to the image
|
|
||||||
service.
|
|
||||||
|
|
||||||
:param context: write context needed for the image service
|
|
||||||
:param timeout_secs: time in seconds to wait for the transfer to complete
|
|
||||||
:param read_file_handle: handle to read data from
|
|
||||||
:param max_data_size: maximum transfer size
|
|
||||||
:param write_file_handle: handle to write data to; if this is None, then
|
|
||||||
param image_service and param image_id should
|
|
||||||
be set.
|
|
||||||
:param image_service: image service handle
|
|
||||||
:param image_id: ID of the image in the image service
|
|
||||||
:param image_meta: image meta-data
|
|
||||||
:raises: ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create the blocking queue
|
|
||||||
blocking_queue = BlockingQueue(BLOCKING_QUEUE_SIZE, max_data_size)
|
|
||||||
|
|
||||||
# Create the image reader
|
|
||||||
reader = FileReadWriteTask(read_file_handle, blocking_queue)
|
|
||||||
|
|
||||||
# Create the image writer
|
|
||||||
if write_file_handle:
|
|
||||||
# File or VMDK in VMware datastore is the image destination
|
|
||||||
writer = FileReadWriteTask(blocking_queue, write_file_handle)
|
|
||||||
elif image_service and image_id:
|
|
||||||
# Image service image is the destination
|
|
||||||
writer = ImageWriter(context,
|
|
||||||
blocking_queue,
|
|
||||||
image_service,
|
|
||||||
image_id,
|
|
||||||
image_meta)
|
|
||||||
else:
|
|
||||||
excep_msg = _("No image destination given.")
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
raise ValueError(excep_msg)
|
|
||||||
|
|
||||||
# Start the reader and writer
|
|
||||||
LOG.debug("Starting image transfer with reader: %(reader)s and writer: "
|
|
||||||
"%(writer)s",
|
|
||||||
{'reader': reader,
|
|
||||||
'writer': writer})
|
|
||||||
reader.start()
|
|
||||||
writer.start()
|
|
||||||
timer = timeout.Timeout(timeout_secs)
|
|
||||||
try:
|
|
||||||
# Wait for the reader and writer to complete
|
|
||||||
reader.wait()
|
|
||||||
writer.wait()
|
|
||||||
except (timeout.Timeout, exceptions.ImageTransferException) as excep:
|
|
||||||
excep_msg = (_("Error occurred during image transfer with reader: "
|
|
||||||
"%(reader)s and writer: %(writer)s") %
|
|
||||||
{'reader': reader,
|
|
||||||
'writer': writer})
|
|
||||||
LOG.exception(excep_msg)
|
|
||||||
reader.stop()
|
|
||||||
writer.stop()
|
|
||||||
|
|
||||||
if isinstance(excep, exceptions.ImageTransferException):
|
|
||||||
raise
|
|
||||||
raise exceptions.ImageTransferException(excep_msg, excep)
|
|
||||||
finally:
|
|
||||||
timer.cancel()
|
|
||||||
read_file_handle.close()
|
|
||||||
if write_file_handle:
|
|
||||||
write_file_handle.close()
|
|
||||||
|
|
||||||
|
|
||||||
def download_image(image, image_meta, session, datastore, rel_path,
|
|
||||||
bypass=True, timeout_secs=7200):
|
|
||||||
"""Transfer an image to a datastore.
|
|
||||||
|
|
||||||
:param image: file-like iterator
|
|
||||||
:param image_meta: image metadata
|
|
||||||
:param session: VMwareAPISession object
|
|
||||||
:param datastore: Datastore object
|
|
||||||
:param rel_path: path where the file will be stored in the datastore
|
|
||||||
:param bypass: if set to True, bypass vCenter to download the image
|
|
||||||
:param timeout_secs: time in seconds to wait for the xfer to complete
|
|
||||||
"""
|
|
||||||
image_size = int(image_meta['size'])
|
|
||||||
method = 'PUT'
|
|
||||||
if bypass:
|
|
||||||
hosts = datastore.get_connected_hosts(session)
|
|
||||||
host = ds_obj.Datastore.choose_host(hosts)
|
|
||||||
host_name = session.invoke_api(vim_util, 'get_object_property',
|
|
||||||
session.vim, host, 'name')
|
|
||||||
ds_url = datastore.build_url(session._scheme, host_name, rel_path,
|
|
||||||
constants.ESX_DATACENTER_PATH)
|
|
||||||
cookie = ds_url.get_transfer_ticket(session, method)
|
|
||||||
conn = ds_url.connect(method, image_size, cookie)
|
|
||||||
else:
|
|
||||||
ds_url = datastore.build_url(session._scheme, session._host, rel_path)
|
|
||||||
cookie = '%s=%s' % (constants.SOAP_COOKIE_KEY,
|
|
||||||
session.vim.get_http_cookie().strip("\""))
|
|
||||||
conn = ds_url.connect(method, image_size, cookie)
|
|
||||||
conn.write = conn.send
|
|
||||||
|
|
||||||
read_handle = rw_handles.ImageReadHandle(image)
|
|
||||||
_start_transfer(None, timeout_secs, read_handle, image_size,
|
|
||||||
write_file_handle=conn)
|
|
||||||
|
|
||||||
|
|
||||||
def download_flat_image(context, timeout_secs, image_service, image_id,
|
|
||||||
**kwargs):
|
|
||||||
"""Download flat image from the image service to VMware server.
|
|
||||||
|
|
||||||
:param context: image service write context
|
|
||||||
:param timeout_secs: time in seconds to wait for the download to complete
|
|
||||||
:param image_service: image service handle
|
|
||||||
:param image_id: ID of the image to be downloaded
|
|
||||||
:param kwargs: keyword arguments to configure the destination
|
|
||||||
file write handle
|
|
||||||
:raises: VimConnectionException, ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
LOG.debug("Downloading image: %s from image service as a flat file.",
|
|
||||||
image_id)
|
|
||||||
|
|
||||||
# TODO(vbala) catch specific exceptions raised by download call
|
|
||||||
read_iter = image_service.download(context, image_id)
|
|
||||||
read_handle = rw_handles.ImageReadHandle(read_iter)
|
|
||||||
file_size = int(kwargs.get('image_size'))
|
|
||||||
write_handle = rw_handles.FileWriteHandle(kwargs.get('host'),
|
|
||||||
kwargs.get('port'),
|
|
||||||
kwargs.get('data_center_name'),
|
|
||||||
kwargs.get('datastore_name'),
|
|
||||||
kwargs.get('cookies'),
|
|
||||||
kwargs.get('file_path'),
|
|
||||||
file_size,
|
|
||||||
cacerts=kwargs.get('cacerts'))
|
|
||||||
_start_transfer(context,
|
|
||||||
timeout_secs,
|
|
||||||
read_handle,
|
|
||||||
file_size,
|
|
||||||
write_file_handle=write_handle)
|
|
||||||
LOG.debug("Downloaded image: %s from image service as a flat file.",
|
|
||||||
image_id)
|
|
||||||
|
|
||||||
|
|
||||||
def download_stream_optimized_data(context, timeout_secs, read_handle,
|
|
||||||
**kwargs):
|
|
||||||
"""Download stream optimized data to VMware server.
|
|
||||||
|
|
||||||
:param context: image service write context
|
|
||||||
:param timeout_secs: time in seconds to wait for the download to complete
|
|
||||||
:param read_handle: handle from which to read the image data
|
|
||||||
:param kwargs: keyword arguments to configure the destination
|
|
||||||
VMDK write handle
|
|
||||||
:returns: managed object reference of the VM created for import to VMware
|
|
||||||
server
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException,
|
|
||||||
ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
file_size = int(kwargs.get('image_size'))
|
|
||||||
write_handle = rw_handles.VmdkWriteHandle(kwargs.get('session'),
|
|
||||||
kwargs.get('host'),
|
|
||||||
kwargs.get('port'),
|
|
||||||
kwargs.get('resource_pool'),
|
|
||||||
kwargs.get('vm_folder'),
|
|
||||||
kwargs.get('vm_import_spec'),
|
|
||||||
file_size)
|
|
||||||
_start_transfer(context,
|
|
||||||
timeout_secs,
|
|
||||||
read_handle,
|
|
||||||
file_size,
|
|
||||||
write_file_handle=write_handle)
|
|
||||||
return write_handle.get_imported_vm()
|
|
||||||
|
|
||||||
|
|
||||||
def download_stream_optimized_image(context, timeout_secs, image_service,
|
|
||||||
image_id, **kwargs):
|
|
||||||
"""Download stream optimized image from image service to VMware server.
|
|
||||||
|
|
||||||
:param context: image service write context
|
|
||||||
:param timeout_secs: time in seconds to wait for the download to complete
|
|
||||||
:param image_service: image service handle
|
|
||||||
:param image_id: ID of the image to be downloaded
|
|
||||||
:param kwargs: keyword arguments to configure the destination
|
|
||||||
VMDK write handle
|
|
||||||
:returns: managed object reference of the VM created for import to VMware
|
|
||||||
server
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException,
|
|
||||||
ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
LOG.debug("Downloading image: %s from image service as a stream "
|
|
||||||
"optimized file.",
|
|
||||||
image_id)
|
|
||||||
|
|
||||||
# TODO(vbala) catch specific exceptions raised by download call
|
|
||||||
read_iter = image_service.download(context, image_id)
|
|
||||||
read_handle = rw_handles.ImageReadHandle(read_iter)
|
|
||||||
imported_vm = download_stream_optimized_data(context, timeout_secs,
|
|
||||||
read_handle, **kwargs)
|
|
||||||
|
|
||||||
LOG.debug("Downloaded image: %s from image service as a stream "
|
|
||||||
"optimized file.",
|
|
||||||
image_id)
|
|
||||||
return imported_vm
|
|
||||||
|
|
||||||
|
|
||||||
def copy_stream_optimized_disk(
|
|
||||||
context, timeout_secs, write_handle, **kwargs):
|
|
||||||
"""Copy virtual disk from VMware server to the given write handle.
|
|
||||||
|
|
||||||
:param context: context
|
|
||||||
:param timeout_secs: time in seconds to wait for the copy to complete
|
|
||||||
:param write_handle: copy destination
|
|
||||||
:param kwargs: keyword arguments to configure the source
|
|
||||||
VMDK read handle
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException,
|
|
||||||
ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
vmdk_file_path = kwargs.get('vmdk_file_path')
|
|
||||||
LOG.debug("Copying virtual disk: %(vmdk_path)s to %(dest)s.",
|
|
||||||
{'vmdk_path': vmdk_file_path,
|
|
||||||
'dest': write_handle.name})
|
|
||||||
file_size = kwargs.get('vmdk_size')
|
|
||||||
read_handle = rw_handles.VmdkReadHandle(kwargs.get('session'),
|
|
||||||
kwargs.get('host'),
|
|
||||||
kwargs.get('port'),
|
|
||||||
kwargs.get('vm'),
|
|
||||||
kwargs.get('vmdk_file_path'),
|
|
||||||
file_size)
|
|
||||||
_start_transfer(context, timeout_secs, read_handle, file_size,
|
|
||||||
write_file_handle=write_handle)
|
|
||||||
LOG.debug("Downloaded virtual disk: %s.", vmdk_file_path)
|
|
||||||
|
|
||||||
|
|
||||||
def upload_image(context, timeout_secs, image_service, image_id, owner_id,
|
|
||||||
**kwargs):
|
|
||||||
"""Upload the VM's disk file to image service.
|
|
||||||
|
|
||||||
:param context: image service write context
|
|
||||||
:param timeout_secs: time in seconds to wait for the upload to complete
|
|
||||||
:param image_service: image service handle
|
|
||||||
:param image_id: upload destination image ID
|
|
||||||
:param kwargs: keyword arguments to configure the source
|
|
||||||
VMDK read handle
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException,
|
|
||||||
ImageTransferException, ValueError
|
|
||||||
"""
|
|
||||||
|
|
||||||
LOG.debug("Uploading to image: %s.", image_id)
|
|
||||||
file_size = kwargs.get('vmdk_size')
|
|
||||||
read_handle = rw_handles.VmdkReadHandle(kwargs.get('session'),
|
|
||||||
kwargs.get('host'),
|
|
||||||
kwargs.get('port'),
|
|
||||||
kwargs.get('vm'),
|
|
||||||
kwargs.get('vmdk_file_path'),
|
|
||||||
file_size)
|
|
||||||
|
|
||||||
# Set the image properties. It is important to set the 'size' to 0.
|
|
||||||
# Otherwise, the image service client will use the VM's disk capacity
|
|
||||||
# which will not be the image size after upload, since it is converted
|
|
||||||
# to a stream-optimized sparse disk.
|
|
||||||
image_metadata = {'disk_format': 'vmdk',
|
|
||||||
'is_public': kwargs.get('is_public'),
|
|
||||||
'name': kwargs.get('image_name'),
|
|
||||||
'status': 'active',
|
|
||||||
'container_format': 'bare',
|
|
||||||
'size': 0,
|
|
||||||
'properties': {'vmware_image_version':
|
|
||||||
kwargs.get('image_version'),
|
|
||||||
'vmware_disktype': 'streamOptimized',
|
|
||||||
'owner_id': owner_id}}
|
|
||||||
|
|
||||||
# Passing 0 as the file size since data size to be transferred cannot be
|
|
||||||
# predetermined.
|
|
||||||
_start_transfer(context,
|
|
||||||
timeout_secs,
|
|
||||||
read_handle,
|
|
||||||
0,
|
|
||||||
image_service=image_service,
|
|
||||||
image_id=image_id,
|
|
||||||
image_meta=image_metadata)
|
|
||||||
LOG.debug("Uploaded image: %s.", image_id)
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -12,16 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from oslo.vmware._i18n import _
|
from oslo_vmware.objects.datacenter import * # noqa
|
||||||
|
|
||||||
|
|
||||||
class Datacenter(object):
|
|
||||||
|
|
||||||
def __init__(self, ref, name):
|
|
||||||
"""Datacenter object holds ref and name together for convenience."""
|
|
||||||
if name is None:
|
|
||||||
raise ValueError(_("Datacenter name cannot be None"))
|
|
||||||
if ref is None:
|
|
||||||
raise ValueError(_("Datacenter reference cannot be None"))
|
|
||||||
self.ref = ref
|
|
||||||
self.name = name
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -12,307 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
from oslo_vmware.objects.datastore import * # noqa
|
||||||
import posixpath
|
|
||||||
import random
|
|
||||||
|
|
||||||
import six.moves.http_client as httplib
|
|
||||||
import six.moves.urllib.parse as urlparse
|
|
||||||
|
|
||||||
from oslo.vmware._i18n import _
|
|
||||||
from oslo.vmware import constants
|
|
||||||
from oslo.vmware import exceptions
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Datastore(object):
|
|
||||||
|
|
||||||
def __init__(self, ref, name, capacity=None, freespace=None,
|
|
||||||
type=None, datacenter=None):
|
|
||||||
"""Datastore object holds ref and name together for convenience.
|
|
||||||
|
|
||||||
:param ref: a vSphere reference to a datastore
|
|
||||||
:param name: vSphere unique name for this datastore
|
|
||||||
:param capacity: (optional) capacity in bytes of this datastore
|
|
||||||
:param freespace: (optional) free space in bytes of datastore
|
|
||||||
:param type: (optional) datastore type
|
|
||||||
:param datacenter: (optional) oslo.vmware Datacenter object
|
|
||||||
"""
|
|
||||||
if name is None:
|
|
||||||
raise ValueError(_("Datastore name cannot be None"))
|
|
||||||
if ref is None:
|
|
||||||
raise ValueError(_("Datastore reference cannot be None"))
|
|
||||||
if freespace is not None and capacity is None:
|
|
||||||
raise ValueError(_("Invalid capacity"))
|
|
||||||
if capacity is not None and freespace is not None:
|
|
||||||
if capacity < freespace:
|
|
||||||
raise ValueError(_("Capacity is smaller than free space"))
|
|
||||||
|
|
||||||
self.ref = ref
|
|
||||||
self.name = name
|
|
||||||
self.capacity = capacity
|
|
||||||
self.freespace = freespace
|
|
||||||
self.type = type
|
|
||||||
self.datacenter = datacenter
|
|
||||||
|
|
||||||
def build_path(self, *paths):
|
|
||||||
"""Constructs and returns a DatastorePath.
|
|
||||||
|
|
||||||
:param paths: list of path components, for constructing a path relative
|
|
||||||
to the root directory of the datastore
|
|
||||||
:return: a DatastorePath object
|
|
||||||
"""
|
|
||||||
return DatastorePath(self.name, *paths)
|
|
||||||
|
|
||||||
def build_url(self, scheme, server, rel_path, datacenter_name=None):
|
|
||||||
"""Constructs and returns a DatastoreURL.
|
|
||||||
|
|
||||||
:param scheme: scheme of the URL (http, https).
|
|
||||||
:param server: hostname or ip
|
|
||||||
:param rel_path: relative path of the file on the datastore
|
|
||||||
:param datacenter_name: (optional) datacenter name
|
|
||||||
:return: a DatastoreURL object
|
|
||||||
"""
|
|
||||||
if self.datacenter is None and datacenter_name is None:
|
|
||||||
raise ValueError(_("datacenter must be set to build url"))
|
|
||||||
if datacenter_name is None:
|
|
||||||
datacenter_name = self.datacenter.name
|
|
||||||
return DatastoreURL(scheme, server, rel_path, datacenter_name,
|
|
||||||
self.name)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '[%s]' % self._name
|
|
||||||
|
|
||||||
def get_summary(self, session):
|
|
||||||
"""Get datastore summary.
|
|
||||||
|
|
||||||
:param datastore: Reference to the datastore
|
|
||||||
:return: 'summary' property of the datastore
|
|
||||||
"""
|
|
||||||
return session.invoke_api(vim_util, 'get_object_property',
|
|
||||||
session.vim, self.ref, 'summary')
|
|
||||||
|
|
||||||
def get_connected_hosts(self, session):
|
|
||||||
"""Get a list of usable (accessible, mounted, read-writable) hosts where
|
|
||||||
the datastore is mounted.
|
|
||||||
|
|
||||||
:param: session: session
|
|
||||||
:return: list of HostSystem managed object references
|
|
||||||
"""
|
|
||||||
hosts = []
|
|
||||||
summary = self.get_summary(session)
|
|
||||||
if not summary.accessible:
|
|
||||||
return hosts
|
|
||||||
host_mounts = session.invoke_api(vim_util, 'get_object_property',
|
|
||||||
session.vim, self.ref, 'host')
|
|
||||||
if not hasattr(host_mounts, 'DatastoreHostMount'):
|
|
||||||
return hosts
|
|
||||||
for host_mount in host_mounts.DatastoreHostMount:
|
|
||||||
if self.is_datastore_mount_usable(host_mount.mountInfo):
|
|
||||||
hosts.append(host_mount.key)
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_datastore_mount_usable(mount_info):
|
|
||||||
"""Check if a datastore is usable as per the given mount info.
|
|
||||||
|
|
||||||
The datastore is considered to be usable for a host only if it is
|
|
||||||
writable, mounted and accessible.
|
|
||||||
|
|
||||||
:param mount_info: HostMountInfo data object
|
|
||||||
:return: True if datastore is usable
|
|
||||||
"""
|
|
||||||
writable = mount_info.accessMode == 'readWrite'
|
|
||||||
mounted = getattr(mount_info, 'mounted', True)
|
|
||||||
accessible = getattr(mount_info, 'accessible', False)
|
|
||||||
|
|
||||||
return writable and mounted and accessible
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def choose_host(hosts):
|
|
||||||
i = random.randrange(0, len(hosts))
|
|
||||||
return hosts[i]
|
|
||||||
|
|
||||||
|
|
||||||
class DatastorePath(object):
|
|
||||||
|
|
||||||
"""Class for representing a directory or file path in a vSphere datatore.
|
|
||||||
|
|
||||||
This provides various helper methods to access components and useful
|
|
||||||
variants of the datastore path.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
DatastorePath("datastore1", "_base/foo", "foo.vmdk") creates an
|
|
||||||
object that describes the "[datastore1] _base/foo/foo.vmdk" datastore
|
|
||||||
file path to a virtual disk.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- Datastore path representations always uses forward slash as separator
|
|
||||||
(hence the use of the posixpath module).
|
|
||||||
- Datastore names are enclosed in square brackets.
|
|
||||||
- Path part of datastore path is relative to the root directory
|
|
||||||
of the datastore, and is always separated from the [ds_name] part with
|
|
||||||
a single space.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, datastore_name, *paths):
|
|
||||||
if datastore_name is None or datastore_name == '':
|
|
||||||
raise ValueError(_("Datastore name cannot be empty"))
|
|
||||||
self._datastore_name = datastore_name
|
|
||||||
self._rel_path = ''
|
|
||||||
if paths:
|
|
||||||
if None in paths:
|
|
||||||
raise ValueError(_("Path component cannot be None"))
|
|
||||||
self._rel_path = posixpath.join(*paths)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""Full datastore path to the file or directory."""
|
|
||||||
if self._rel_path != '':
|
|
||||||
return "[%s] %s" % (self._datastore_name, self.rel_path)
|
|
||||||
return "[%s]" % self._datastore_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datastore(self):
|
|
||||||
return self._datastore_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
return DatastorePath(self.datastore, posixpath.dirname(self._rel_path))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def basename(self):
|
|
||||||
return posixpath.basename(self._rel_path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dirname(self):
|
|
||||||
return posixpath.dirname(self._rel_path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rel_path(self):
|
|
||||||
return self._rel_path
|
|
||||||
|
|
||||||
def join(self, *paths):
|
|
||||||
"""Join one or more path components intelligently into a datastore path.
|
|
||||||
|
|
||||||
If any component is an absolute path, all previous components are
|
|
||||||
thrown away, and joining continues. The return value is the
|
|
||||||
concatenation of the paths with exactly one slash ('/') inserted
|
|
||||||
between components, unless p is empty.
|
|
||||||
|
|
||||||
:return: A datastore path
|
|
||||||
"""
|
|
||||||
if paths:
|
|
||||||
if None in paths:
|
|
||||||
raise ValueError(_("Path component cannot be None"))
|
|
||||||
return DatastorePath(self.datastore, self._rel_path, *paths)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (isinstance(other, DatastorePath) and
|
|
||||||
self._datastore_name == other._datastore_name and
|
|
||||||
self._rel_path == other._rel_path)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, datastore_path):
|
|
||||||
"""Constructs a DatastorePath object given a datastore path string."""
|
|
||||||
if not datastore_path:
|
|
||||||
raise ValueError(_("Datastore path cannot be empty"))
|
|
||||||
|
|
||||||
spl = datastore_path.split('[', 1)[1].split(']', 1)
|
|
||||||
path = ""
|
|
||||||
if len(spl) == 1:
|
|
||||||
datastore_name = spl[0]
|
|
||||||
else:
|
|
||||||
datastore_name, path = spl
|
|
||||||
return cls(datastore_name, path.strip())
|
|
||||||
|
|
||||||
|
|
||||||
class DatastoreURL(object):
|
|
||||||
|
|
||||||
"""Class for representing a URL to HTTP access a file in a datastore.
|
|
||||||
|
|
||||||
This provides various helper methods to access components and useful
|
|
||||||
variants of the datastore URL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, scheme, server, path, datacenter_path, datastore_name):
|
|
||||||
self._scheme = scheme
|
|
||||||
self._server = server
|
|
||||||
self._path = path
|
|
||||||
self._datacenter_path = datacenter_path
|
|
||||||
self._datastore_name = datastore_name
|
|
||||||
params = {'dcPath': self._datacenter_path,
|
|
||||||
'dsName': self._datastore_name}
|
|
||||||
self._query = urlparse.urlencode(params)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def urlparse(cls, url):
|
|
||||||
scheme, server, path, params, query, fragment = urlparse.urlparse(url)
|
|
||||||
if not query:
|
|
||||||
path = path.split('?')
|
|
||||||
query = path[1]
|
|
||||||
path = path[0]
|
|
||||||
params = urlparse.parse_qs(query)
|
|
||||||
dc_path = params.get('dcPath')
|
|
||||||
if dc_path is not None and len(dc_path) > 0:
|
|
||||||
datacenter_path = dc_path[0]
|
|
||||||
ds_name = params.get('dsName')
|
|
||||||
if ds_name is not None and len(ds_name) > 0:
|
|
||||||
datastore_name = ds_name[0]
|
|
||||||
path = path[len('/folder'):]
|
|
||||||
return cls(scheme, server, path, datacenter_path, datastore_name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
return self._path.strip('/')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datacenter_path(self):
|
|
||||||
return self._datacenter_path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datastore_name(self):
|
|
||||||
return self._datastore_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '%s://%s/folder/%s?%s' % (self._scheme, self._server,
|
|
||||||
self.path, self._query)
|
|
||||||
|
|
||||||
def connect(self, method, content_length, cookie):
|
|
||||||
try:
|
|
||||||
if self._scheme == 'http':
|
|
||||||
conn = httplib.HTTPConnection(self._server)
|
|
||||||
elif self._scheme == 'https':
|
|
||||||
conn = httplib.HTTPSConnection(self._server)
|
|
||||||
else:
|
|
||||||
excep_msg = _("Invalid scheme: %s.") % self._scheme
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
raise ValueError(excep_msg)
|
|
||||||
conn.putrequest(method, '/folder/%s?%s' % (self.path, self._query))
|
|
||||||
conn.putheader('User-Agent', constants.USER_AGENT)
|
|
||||||
conn.putheader('Content-Length', content_length)
|
|
||||||
conn.putheader('Cookie', cookie)
|
|
||||||
conn.endheaders()
|
|
||||||
LOG.debug("Created HTTP connection to transfer the file with "
|
|
||||||
"URL = %s.", str(self))
|
|
||||||
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.") % str(self)
|
|
||||||
LOG.exception(excep_msg)
|
|
||||||
raise exceptions.VimConnectionException(excep_msg, excep)
|
|
||||||
|
|
||||||
def get_transfer_ticket(self, session, method):
|
|
||||||
client_factory = session.vim.client.factory
|
|
||||||
spec = vim_util.get_http_service_request_spec(client_factory, method,
|
|
||||||
str(self))
|
|
||||||
ticket = session.invoke_api(
|
|
||||||
session.vim,
|
|
||||||
'AcquireGenericServiceTicket',
|
|
||||||
session.vim.service_content.sessionManager,
|
|
||||||
spec=spec)
|
|
||||||
return '%s="%s"' % (constants.CGI_COOKIE_KEY, ticket.id)
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,188 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.pbm import * # noqa
|
||||||
VMware PBM service client and PBM related utility methods
|
|
||||||
|
|
||||||
PBM is used for policy based placement in VMware datastores.
|
|
||||||
Refer http://goo.gl/GR2o6U for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
import six.moves.urllib.parse as urlparse
|
|
||||||
import six.moves.urllib.request as urllib
|
|
||||||
import suds.sax.element as element
|
|
||||||
|
|
||||||
from oslo.vmware._i18n import _LW
|
|
||||||
from oslo.vmware import service
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
|
|
||||||
SERVICE_TYPE = 'PbmServiceInstance'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Pbm(service.Service):
|
|
||||||
"""Service class that provides access to the Storage Policy API."""
|
|
||||||
|
|
||||||
def __init__(self, protocol='https', host='localhost', port=443,
|
|
||||||
wsdl_url=None, cacert=None, insecure=True):
|
|
||||||
"""Constructs a PBM service client object.
|
|
||||||
|
|
||||||
:param protocol: http or https
|
|
||||||
:param host: server IP address or host name
|
|
||||||
:param port: port for connection
|
|
||||||
:param wsdl_url: PBM WSDL url
|
|
||||||
:param cacert: Specify a CA bundle file to use in verifying a
|
|
||||||
TLS (https) server certificate.
|
|
||||||
:param insecure: Verify HTTPS connections using system certificates,
|
|
||||||
used only if cacert is not specified
|
|
||||||
"""
|
|
||||||
base_url = service.Service.build_base_url(protocol, host, port)
|
|
||||||
soap_url = base_url + '/pbm'
|
|
||||||
super(Pbm, self).__init__(wsdl_url, soap_url, cacert, insecure)
|
|
||||||
|
|
||||||
def set_soap_cookie(self, cookie):
|
|
||||||
"""Set the specified vCenter session cookie in the SOAP header
|
|
||||||
|
|
||||||
:param cookie: cookie to set
|
|
||||||
"""
|
|
||||||
elem = element.Element('vcSessionCookie').setText(cookie)
|
|
||||||
self.client.set_options(soapheaders=elem)
|
|
||||||
|
|
||||||
def retrieve_service_content(self):
|
|
||||||
ref = vim_util.get_moref(service.SERVICE_INSTANCE, SERVICE_TYPE)
|
|
||||||
return self.PbmRetrieveServiceContent(ref)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "PBM Object"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "PBM Object"
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_profiles(session):
|
|
||||||
"""Get all the profiles defined in VC server.
|
|
||||||
|
|
||||||
:returns: PbmProfile data objects
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
LOG.debug("Fetching all the profiles defined in VC server.")
|
|
||||||
|
|
||||||
pbm = session.pbm
|
|
||||||
profile_manager = pbm.service_content.profileManager
|
|
||||||
res_type = pbm.client.factory.create('ns0:PbmProfileResourceType')
|
|
||||||
res_type.resourceType = 'STORAGE'
|
|
||||||
profiles = []
|
|
||||||
profile_ids = session.invoke_api(pbm,
|
|
||||||
'PbmQueryProfile',
|
|
||||||
profile_manager,
|
|
||||||
resourceType=res_type)
|
|
||||||
LOG.debug("Fetched profile IDs: %s.", profile_ids)
|
|
||||||
if profile_ids:
|
|
||||||
profiles = session.invoke_api(pbm,
|
|
||||||
'PbmRetrieveContent',
|
|
||||||
profile_manager,
|
|
||||||
profileIds=profile_ids)
|
|
||||||
return profiles
|
|
||||||
|
|
||||||
|
|
||||||
def get_profile_id_by_name(session, profile_name):
|
|
||||||
"""Get the profile UUID corresponding to the given profile name.
|
|
||||||
|
|
||||||
:param profile_name: profile name whose UUID needs to be retrieved
|
|
||||||
:returns: profile UUID string or None if profile not found
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
LOG.debug("Retrieving profile ID for profile: %s.", profile_name)
|
|
||||||
for profile in get_all_profiles(session):
|
|
||||||
if profile.name == profile_name:
|
|
||||||
profile_id = profile.profileId
|
|
||||||
LOG.debug("Retrieved profile ID: %(id)s for profile: %(name)s.",
|
|
||||||
{'id': profile_id,
|
|
||||||
'name': profile_name})
|
|
||||||
return profile_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def filter_hubs_by_profile(session, hubs, profile_id):
|
|
||||||
"""Filter and return hubs that match the given profile.
|
|
||||||
|
|
||||||
:param hubs: PbmPlacementHub morefs
|
|
||||||
:param profile_id: profile ID
|
|
||||||
:returns: subset of hubs that match the given profile
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
LOG.debug("Filtering hubs: %(hubs)s that match profile: %(profile)s.",
|
|
||||||
{'hubs': hubs,
|
|
||||||
'profile': profile_id})
|
|
||||||
|
|
||||||
pbm = session.pbm
|
|
||||||
placement_solver = pbm.service_content.placementSolver
|
|
||||||
filtered_hubs = session.invoke_api(pbm,
|
|
||||||
'PbmQueryMatchingHub',
|
|
||||||
placement_solver,
|
|
||||||
hubsToSearch=hubs,
|
|
||||||
profile=profile_id)
|
|
||||||
LOG.debug("Filtered hubs: %s", filtered_hubs)
|
|
||||||
return filtered_hubs
|
|
||||||
|
|
||||||
|
|
||||||
def convert_datastores_to_hubs(pbm_client_factory, datastores):
|
|
||||||
"""Convert given datastore morefs to PbmPlacementHub morefs.
|
|
||||||
|
|
||||||
:param pbm_client_factory: Factory to create PBM API input specs
|
|
||||||
:param datastores: list of datastore morefs
|
|
||||||
:returns: list of PbmPlacementHub morefs
|
|
||||||
"""
|
|
||||||
hubs = []
|
|
||||||
for ds in datastores:
|
|
||||||
hub = pbm_client_factory.create('ns0:PbmPlacementHub')
|
|
||||||
hub.hubId = ds.value
|
|
||||||
hub.hubType = 'Datastore'
|
|
||||||
hubs.append(hub)
|
|
||||||
return hubs
|
|
||||||
|
|
||||||
|
|
||||||
def filter_datastores_by_hubs(hubs, datastores):
|
|
||||||
"""Get filtered subset of datastores corresponding to the given hub list.
|
|
||||||
|
|
||||||
:param hubs: list of PbmPlacementHub morefs
|
|
||||||
:param datastores: all candidate datastores
|
|
||||||
:returns: subset of datastores corresponding to the given hub list
|
|
||||||
"""
|
|
||||||
filtered_dss = []
|
|
||||||
hub_ids = [hub.hubId for hub in hubs]
|
|
||||||
for ds in datastores:
|
|
||||||
if ds.value in hub_ids:
|
|
||||||
filtered_dss.append(ds)
|
|
||||||
return filtered_dss
|
|
||||||
|
|
||||||
|
|
||||||
def get_pbm_wsdl_location(vc_version):
|
|
||||||
"""Return PBM WSDL file location corresponding to VC version.
|
|
||||||
|
|
||||||
:param vc_version: a dot-separated version string. For example, "1.2".
|
|
||||||
:return: the pbm wsdl file location.
|
|
||||||
"""
|
|
||||||
if not vc_version:
|
|
||||||
return
|
|
||||||
ver = vc_version.split('.')
|
|
||||||
major_minor = ver[0]
|
|
||||||
if len(ver) >= 2:
|
|
||||||
major_minor = '%s.%s' % (major_minor, ver[1])
|
|
||||||
curr_dir = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
pbm_service_wsdl = os.path.join(curr_dir, 'wsdl', major_minor,
|
|
||||||
'pbmService.wsdl')
|
|
||||||
if not os.path.exists(pbm_service_wsdl):
|
|
||||||
LOG.warn(_LW("PBM WSDL file %s not found."), pbm_service_wsdl)
|
|
||||||
return
|
|
||||||
pbm_wsdl = urlparse.urljoin('file:', urllib.pathname2url(pbm_service_wsdl))
|
|
||||||
LOG.debug("Using PBM WSDL location: %s.", pbm_wsdl)
|
|
||||||
return pbm_wsdl
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,620 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.rw_handles import * # noqa
|
||||||
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 logging
|
|
||||||
import ssl
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
import six.moves.urllib.parse as urlparse
|
|
||||||
from urllib3 import connection as httplib
|
|
||||||
|
|
||||||
from oslo.utils import excutils
|
|
||||||
from oslo.utils import netutils
|
|
||||||
from oslo.vmware._i18n import _, _LE, _LW
|
|
||||||
from oslo.vmware import exceptions
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MIN_PROGRESS_DIFF_TO_LOG = 25
|
|
||||||
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
|
|
||||||
self._last_logged_progress = 0
|
|
||||||
|
|
||||||
def _create_read_connection(self, url, cookies=None, cacerts=False):
|
|
||||||
LOG.debug("Opening URL: %s for reading.", url)
|
|
||||||
try:
|
|
||||||
headers = {'User-Agent': USER_AGENT}
|
|
||||||
if cookies:
|
|
||||||
headers.update({'Cookie':
|
|
||||||
self._build_vim_cookie_header(cookies)})
|
|
||||||
response = requests.get(url, headers=headers, stream=True,
|
|
||||||
verify=cacerts)
|
|
||||||
return response.raw
|
|
||||||
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 _create_write_connection(self, url,
|
|
||||||
file_size=None,
|
|
||||||
cookies=None,
|
|
||||||
overwrite=None,
|
|
||||||
content_type=None,
|
|
||||||
cacerts=False):
|
|
||||||
"""Create HTTP connection to write to VMDK file."""
|
|
||||||
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)
|
|
||||||
cert_reqs = None
|
|
||||||
|
|
||||||
# cacerts can be either True or False or contain
|
|
||||||
# actual certificates. If it is a boolean, then
|
|
||||||
# we need to set cert_reqs and clear the cacerts
|
|
||||||
if isinstance(cacerts, bool):
|
|
||||||
if cacerts:
|
|
||||||
cert_reqs = ssl.CERT_REQUIRED
|
|
||||||
else:
|
|
||||||
cert_reqs = ssl.CERT_NONE
|
|
||||||
cacerts = None
|
|
||||||
|
|
||||||
conn.set_cert(ca_certs=cacerts, cert_reqs=cert_reqs)
|
|
||||||
else:
|
|
||||||
excep_msg = _("Invalid scheme: %s.") % scheme
|
|
||||||
LOG.error(excep_msg)
|
|
||||||
raise ValueError(excep_msg)
|
|
||||||
|
|
||||||
if query:
|
|
||||||
path = path + '?' + query
|
|
||||||
|
|
||||||
headers = {'User-Agent': USER_AGENT}
|
|
||||||
if file_size:
|
|
||||||
headers.update({'Content-Length': str(file_size)})
|
|
||||||
if overwrite:
|
|
||||||
headers.update({'Overwrite': overwrite})
|
|
||||||
if cookies:
|
|
||||||
headers.update({'Cookie':
|
|
||||||
self._build_vim_cookie_header(cookies)})
|
|
||||||
if content_type:
|
|
||||||
headers.update({'Content-Type': content_type})
|
|
||||||
|
|
||||||
conn.putrequest('PUT', path)
|
|
||||||
for key, value in six.iteritems(headers):
|
|
||||||
conn.putheader(key, value)
|
|
||||||
conn.endheaders()
|
|
||||||
return conn
|
|
||||||
except requests.RequestException 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 close(self):
|
|
||||||
"""Close the file handle."""
|
|
||||||
try:
|
|
||||||
self._file_handle.close()
|
|
||||||
except Exception:
|
|
||||||
LOG.warn(_LW("Error occurred while closing the file handle"),
|
|
||||||
exc_info=True)
|
|
||||||
|
|
||||||
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 _get_soap_url(self, scheme, host, port):
|
|
||||||
"""Returns the IPv4/v6 compatible SOAP URL for the given host."""
|
|
||||||
if netutils.is_valid_ipv6(host):
|
|
||||||
return '%s://[%s]:%d' % (scheme, host, port)
|
|
||||||
return '%s://%s:%d' % (scheme, host, port)
|
|
||||||
|
|
||||||
def _fix_esx_url(self, url, host, port):
|
|
||||||
"""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
|
|
||||||
if netutils.is_valid_ipv6(host):
|
|
||||||
netloc = '[%s]:%d' % (host, port)
|
|
||||||
else:
|
|
||||||
netloc = "%s:%d" % (host, port)
|
|
||||||
url = urlparse.urlunparse((scheme,
|
|
||||||
netloc,
|
|
||||||
path,
|
|
||||||
params,
|
|
||||||
query,
|
|
||||||
fragment))
|
|
||||||
return url
|
|
||||||
|
|
||||||
def _find_vmdk_url(self, lease_info, host, port):
|
|
||||||
"""Find the URL corresponding to a VMDK file in lease info."""
|
|
||||||
url = None
|
|
||||||
for deviceUrl in lease_info.deviceUrl:
|
|
||||||
if deviceUrl.disk:
|
|
||||||
url = self._fix_esx_url(deviceUrl.url, host, port)
|
|
||||||
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
|
|
||||||
|
|
||||||
def _log_progress(self, progress):
|
|
||||||
"""Log data transfer progress."""
|
|
||||||
if (progress == 100 or (progress - self._last_logged_progress >=
|
|
||||||
MIN_PROGRESS_DIFF_TO_LOG)):
|
|
||||||
LOG.debug("Data transfer progress is %d%%.", progress)
|
|
||||||
self._last_logged_progress = progress
|
|
||||||
|
|
||||||
|
|
||||||
class FileWriteHandle(FileHandle):
|
|
||||||
"""Write handle for a file in VMware server."""
|
|
||||||
|
|
||||||
def __init__(self, host, port, data_center_name, datastore_name, cookies,
|
|
||||||
file_path, file_size, scheme='https', cacerts=False):
|
|
||||||
"""Initializes the write handle with given parameters.
|
|
||||||
|
|
||||||
:param host: ESX/VC server IP address or host name
|
|
||||||
:param port: port for connection
|
|
||||||
: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, port)
|
|
||||||
param_list = {'dcPath': data_center_name, 'dsName': datastore_name}
|
|
||||||
self._url = '%s/folder/%s' % (soap_url, file_path)
|
|
||||||
self._url = self._url + '?' + urlparse.urlencode(param_list)
|
|
||||||
|
|
||||||
self._conn = self._create_write_connection(self._url,
|
|
||||||
file_size,
|
|
||||||
cookies=cookies,
|
|
||||||
cacerts=cacerts)
|
|
||||||
FileHandle.__init__(self, self._conn)
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
"""Write data to the file.
|
|
||||||
|
|
||||||
:param data: data to be written
|
|
||||||
:raises: VimConnectionException, VimException
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._file_handle.send(data)
|
|
||||||
except requests.RequestException 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(_LW("Error occurred while reading the HTTP response."),
|
|
||||||
exc_info=True)
|
|
||||||
super(FileWriteHandle, self).close()
|
|
||||||
|
|
||||||
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, port, 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 or host name
|
|
||||||
:param port: port for connection
|
|
||||||
: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, port)
|
|
||||||
self._vm_ref = lease_info.entity
|
|
||||||
|
|
||||||
cookies = session.vim.client.options.transport.cookiejar
|
|
||||||
# Create HTTP connection to write to VMDK URL
|
|
||||||
octet_stream = 'binary/octet-stream'
|
|
||||||
self._conn = self._create_write_connection(self._url,
|
|
||||||
vmdk_size,
|
|
||||||
cookies=cookies,
|
|
||||||
overwrite='t',
|
|
||||||
content_type=octet_stream,
|
|
||||||
cacerts=session._cacert)
|
|
||||||
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 write(self, data):
|
|
||||||
"""Write data to the file.
|
|
||||||
|
|
||||||
:param data: data to be written
|
|
||||||
:raises: VimConnectionException, VimException
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._file_handle.send(data)
|
|
||||||
self._bytes_written += len(data)
|
|
||||||
except requests.RequestException 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)
|
|
||||||
|
|
||||||
# TODO(vbala) Move this method to FileHandle.
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
progress = int(float(self._bytes_written) / self._vmdk_size * 100)
|
|
||||||
self._log_progress(progress)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._session.invoke_api(self._session.vim,
|
|
||||||
'HttpNfcLeaseProgress',
|
|
||||||
self._lease,
|
|
||||||
percent=progress)
|
|
||||||
except exceptions.VimException:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.exception(_LE("Error occurred while updating the "
|
|
||||||
"write progress of VMDK file with "
|
|
||||||
"URL = %s."),
|
|
||||||
self._url)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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(_LW("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, port, 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 or host name
|
|
||||||
:param port: port for connection
|
|
||||||
: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, port)
|
|
||||||
cookies = session.vim.client.options.transport.cookiejar
|
|
||||||
cacerts = session.vim.client.options.transport.verify
|
|
||||||
self._conn = self._create_read_connection(self._url,
|
|
||||||
cookies=cookies,
|
|
||||||
cacerts=cacerts)
|
|
||||||
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 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
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = self._file_handle.read(READ_CHUNKSIZE)
|
|
||||||
self._bytes_read += len(data)
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
progress = int(float(self._bytes_read) / self._vmdk_size * 100)
|
|
||||||
self._log_progress(progress)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._session.invoke_api(self._session.vim,
|
|
||||||
'HttpNfcLeaseProgress',
|
|
||||||
self._lease,
|
|
||||||
percent=progress)
|
|
||||||
except exceptions.VimException:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.exception(_LE("Error occurred while updating the "
|
|
||||||
"read progress of VMDK file with URL = %s."),
|
|
||||||
self._url)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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(_LW("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 = next(self._iter)
|
|
||||||
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"
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,345 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.service import * # noqa
|
||||||
Common classes that provide access to vSphere services.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
import six.moves.http_client as httplib
|
|
||||||
import suds
|
|
||||||
from suds import cache
|
|
||||||
from suds import client
|
|
||||||
from suds import plugin
|
|
||||||
from suds import transport
|
|
||||||
|
|
||||||
from oslo.utils import timeutils
|
|
||||||
from oslo.vmware._i18n import _
|
|
||||||
from oslo.vmware import exceptions
|
|
||||||
from oslo.vmware import vim_util
|
|
||||||
|
|
||||||
CACHE_TIMEOUT = 60 * 60 # One hour cache timeout
|
|
||||||
ADDRESS_IN_USE_ERROR = 'Address already in use'
|
|
||||||
CONN_ABORT_ERROR = 'Software caused connection abort'
|
|
||||||
RESP_NOT_XML_ERROR = 'Response is "text/html", not "text/xml"'
|
|
||||||
|
|
||||||
SERVICE_INSTANCE = 'ServiceInstance'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceMessagePlugin(plugin.MessagePlugin):
|
|
||||||
"""Suds plug-in handling some special cases while calling VI SDK."""
|
|
||||||
|
|
||||||
def add_attribute_for_value(self, node):
|
|
||||||
"""Helper to handle AnyType.
|
|
||||||
|
|
||||||
Suds does not handle AnyType properly. But VI SDK requires type
|
|
||||||
attribute to be set when AnyType is used.
|
|
||||||
|
|
||||||
:param node: XML value node
|
|
||||||
"""
|
|
||||||
if node.name == 'value':
|
|
||||||
node.set('xsi:type', 'xsd:string')
|
|
||||||
|
|
||||||
def marshalled(self, context):
|
|
||||||
"""Modifies the envelope document before it is sent.
|
|
||||||
|
|
||||||
This method provides the plug-in with the opportunity to prune empty
|
|
||||||
nodes and fix nodes before sending it to the server.
|
|
||||||
|
|
||||||
:param context: send context
|
|
||||||
"""
|
|
||||||
# Suds builds the entire request object based on the WSDL schema.
|
|
||||||
# VI SDK throws server errors if optional SOAP nodes are sent
|
|
||||||
# without values; e.g., <test/> as opposed to <test>test</test>.
|
|
||||||
context.envelope.prune()
|
|
||||||
context.envelope.walk(self.add_attribute_for_value)
|
|
||||||
|
|
||||||
|
|
||||||
class Response(six.BytesIO):
|
|
||||||
"""Response with an input stream as source."""
|
|
||||||
|
|
||||||
def __init__(self, stream, status=200, headers=None):
|
|
||||||
self.status = status
|
|
||||||
self.headers = headers or {}
|
|
||||||
self.reason = requests.status_codes._codes.get(
|
|
||||||
status, [''])[0].upper().replace('_', ' ')
|
|
||||||
six.BytesIO.__init__(self, stream)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _original_response(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
@property
|
|
||||||
def msg(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def read(self, chunk_size, **kwargs):
|
|
||||||
return six.BytesIO.read(self, chunk_size)
|
|
||||||
|
|
||||||
def info(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_all(self, name, default):
|
|
||||||
result = self.headers.get(name)
|
|
||||||
if not result:
|
|
||||||
return default
|
|
||||||
return [result]
|
|
||||||
|
|
||||||
def getheaders(self, name):
|
|
||||||
return self.get_all(name, [])
|
|
||||||
|
|
||||||
def release_conn(self):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class LocalFileAdapter(requests.adapters.HTTPAdapter):
|
|
||||||
"""Transport adapter for local files.
|
|
||||||
|
|
||||||
See http://stackoverflow.com/a/22989322
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _build_response_from_file(self, request):
|
|
||||||
file_path = request.url[7:]
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
buff = bytearray(os.path.getsize(file_path))
|
|
||||||
f.readinto(buff)
|
|
||||||
resp = Response(buff)
|
|
||||||
return self.build_response(request, resp)
|
|
||||||
|
|
||||||
def send(self, request, stream=False, timeout=None,
|
|
||||||
verify=True, cert=None, proxies=None):
|
|
||||||
return self._build_response_from_file(request)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestsTransport(transport.Transport):
|
|
||||||
def __init__(self, cacert=None, insecure=True):
|
|
||||||
transport.Transport.__init__(self)
|
|
||||||
# insecure flag is used only if cacert is not
|
|
||||||
# specified.
|
|
||||||
self.verify = cacert if cacert else not insecure
|
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.mount('file:///', LocalFileAdapter())
|
|
||||||
self.cookiejar = self.session.cookies
|
|
||||||
|
|
||||||
def open(self, request):
|
|
||||||
resp = self.session.get(request.url, verify=self.verify)
|
|
||||||
return six.StringIO(resp.content)
|
|
||||||
|
|
||||||
def send(self, request):
|
|
||||||
resp = self.session.post(request.url,
|
|
||||||
data=request.message,
|
|
||||||
headers=request.headers,
|
|
||||||
verify=self.verify)
|
|
||||||
return transport.Reply(resp.status_code, resp.headers, resp.content)
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryCache(cache.ObjectCache):
|
|
||||||
def __init__(self):
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
"""Retrieves the value for a key or None."""
|
|
||||||
now = timeutils.utcnow_ts()
|
|
||||||
for k in list(self._cache):
|
|
||||||
(timeout, _value) = self._cache[k]
|
|
||||||
if timeout and now >= timeout:
|
|
||||||
del self._cache[k]
|
|
||||||
|
|
||||||
return self._cache.get(key, (0, None))[1]
|
|
||||||
|
|
||||||
def put(self, key, value, time=CACHE_TIMEOUT):
|
|
||||||
"""Sets the value for a key."""
|
|
||||||
timeout = 0
|
|
||||||
if time != 0:
|
|
||||||
timeout = timeutils.utcnow_ts() + time
|
|
||||||
self._cache[key] = (timeout, value)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
_CACHE = MemoryCache()
|
|
||||||
|
|
||||||
|
|
||||||
class Service(object):
|
|
||||||
"""Base class containing common functionality for invoking vSphere
|
|
||||||
services
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, wsdl_url=None, soap_url=None,
|
|
||||||
cacert=None, insecure=True):
|
|
||||||
self.wsdl_url = wsdl_url
|
|
||||||
self.soap_url = soap_url
|
|
||||||
LOG.debug("Creating suds client with soap_url='%s' and wsdl_url='%s'",
|
|
||||||
self.soap_url, self.wsdl_url)
|
|
||||||
transport = RequestsTransport(cacert, insecure)
|
|
||||||
self.client = client.Client(self.wsdl_url,
|
|
||||||
transport=transport,
|
|
||||||
location=self.soap_url,
|
|
||||||
plugins=[ServiceMessagePlugin()],
|
|
||||||
cache=_CACHE)
|
|
||||||
self._service_content = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_base_url(protocol, host, port):
|
|
||||||
proto_str = '%s://' % protocol
|
|
||||||
host_str = '[%s]' % host if netaddr.valid_ipv6(host) else host
|
|
||||||
port_str = '' if port is None else ':%d' % port
|
|
||||||
return proto_str + host_str + port_str
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _retrieve_properties_ex_fault_checker(response):
|
|
||||||
"""Checks the RetrievePropertiesEx API response for errors.
|
|
||||||
|
|
||||||
Certain faults are sent in the SOAP body as a property of missingSet.
|
|
||||||
This method raises VimFaultException when a fault is found in the
|
|
||||||
response.
|
|
||||||
|
|
||||||
:param response: response from RetrievePropertiesEx API call
|
|
||||||
:raises: VimFaultException
|
|
||||||
"""
|
|
||||||
fault_list = []
|
|
||||||
details = {}
|
|
||||||
if not response:
|
|
||||||
# This is the case when the session has timed out. ESX SOAP
|
|
||||||
# server sends an empty RetrievePropertiesExResponse. Normally
|
|
||||||
# missingSet in the response objects has the specifics about
|
|
||||||
# the error, but that's not the case with a timed out idle
|
|
||||||
# session. It is as bad as a terminated session for we cannot
|
|
||||||
# use the session. Therefore setting fault to NotAuthenticated
|
|
||||||
# fault.
|
|
||||||
LOG.debug("RetrievePropertiesEx API response is empty; setting "
|
|
||||||
"fault to %s.",
|
|
||||||
exceptions.NOT_AUTHENTICATED)
|
|
||||||
fault_list = [exceptions.NOT_AUTHENTICATED]
|
|
||||||
else:
|
|
||||||
for obj_cont in response.objects:
|
|
||||||
if hasattr(obj_cont, 'missingSet'):
|
|
||||||
for missing_elem in obj_cont.missingSet:
|
|
||||||
f_type = missing_elem.fault.fault
|
|
||||||
f_name = f_type.__class__.__name__
|
|
||||||
fault_list.append(f_name)
|
|
||||||
if f_name == exceptions.NO_PERMISSION:
|
|
||||||
details['object'] = f_type.object.value
|
|
||||||
details['privilegeId'] = f_type.privilegeId
|
|
||||||
|
|
||||||
if fault_list:
|
|
||||||
fault_string = _("Error occurred while calling "
|
|
||||||
"RetrievePropertiesEx.")
|
|
||||||
raise exceptions.VimFaultException(fault_list,
|
|
||||||
fault_string,
|
|
||||||
details=details)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def service_content(self):
|
|
||||||
if self._service_content is None:
|
|
||||||
self._service_content = self.retrieve_service_content()
|
|
||||||
return self._service_content
|
|
||||||
|
|
||||||
def get_http_cookie(self):
|
|
||||||
"""Return the vCenter session cookie."""
|
|
||||||
cookies = self.client.options.transport.cookiejar
|
|
||||||
for cookie in cookies:
|
|
||||||
if cookie.name.lower() == 'vmware_soap_session':
|
|
||||||
return cookie.value
|
|
||||||
|
|
||||||
def __getattr__(self, attr_name):
|
|
||||||
"""Returns the method to invoke API identified by param attr_name."""
|
|
||||||
|
|
||||||
def request_handler(managed_object, **kwargs):
|
|
||||||
"""Handler for vSphere API calls.
|
|
||||||
|
|
||||||
Invokes the API and parses the response for fault checking and
|
|
||||||
other errors.
|
|
||||||
|
|
||||||
:param managed_object: managed object reference argument of the
|
|
||||||
API call
|
|
||||||
:param kwargs: keyword arguments of the API call
|
|
||||||
:returns: response of the API call
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if isinstance(managed_object, str):
|
|
||||||
# For strings, use string value for value and type
|
|
||||||
# of the managed object.
|
|
||||||
managed_object = vim_util.get_moref(managed_object,
|
|
||||||
managed_object)
|
|
||||||
if managed_object is None:
|
|
||||||
return
|
|
||||||
request = getattr(self.client.service, attr_name)
|
|
||||||
response = request(managed_object, **kwargs)
|
|
||||||
if (attr_name.lower() == 'retrievepropertiesex'):
|
|
||||||
Service._retrieve_properties_ex_fault_checker(response)
|
|
||||||
return response
|
|
||||||
except exceptions.VimFaultException:
|
|
||||||
# Catch the VimFaultException that is raised by the fault
|
|
||||||
# check of the SOAP response.
|
|
||||||
raise
|
|
||||||
|
|
||||||
except suds.WebFault as excep:
|
|
||||||
fault_string = None
|
|
||||||
if excep.fault:
|
|
||||||
fault_string = excep.fault.faultstring
|
|
||||||
|
|
||||||
doc = excep.document
|
|
||||||
detail = None
|
|
||||||
if doc is not None:
|
|
||||||
detail = doc.childAtPath('/detail')
|
|
||||||
if not detail:
|
|
||||||
# NOTE(arnaud): this is needed with VC 5.1
|
|
||||||
detail = doc.childAtPath('/Envelope/Body/Fault/detail')
|
|
||||||
fault_list = []
|
|
||||||
details = {}
|
|
||||||
if detail:
|
|
||||||
for fault in detail.getChildren():
|
|
||||||
fault_list.append(fault.get("type"))
|
|
||||||
for child in fault.getChildren():
|
|
||||||
details[child.name] = child.getText()
|
|
||||||
raise exceptions.VimFaultException(fault_list, fault_string,
|
|
||||||
excep, details)
|
|
||||||
|
|
||||||
except AttributeError as excep:
|
|
||||||
raise exceptions.VimAttributeException(
|
|
||||||
_("No such SOAP method %s.") % attr_name, excep)
|
|
||||||
|
|
||||||
except (httplib.CannotSendRequest,
|
|
||||||
httplib.ResponseNotReady,
|
|
||||||
httplib.CannotSendHeader) as excep:
|
|
||||||
raise exceptions.VimSessionOverLoadException(
|
|
||||||
_("httplib error in %s.") % attr_name, excep)
|
|
||||||
|
|
||||||
except requests.RequestException as excep:
|
|
||||||
raise exceptions.VimConnectionException(
|
|
||||||
_("requests error in %s.") % attr_name, excep)
|
|
||||||
|
|
||||||
except Exception as excep:
|
|
||||||
# TODO(vbala) should catch specific exceptions and raise
|
|
||||||
# appropriate VimExceptions.
|
|
||||||
|
|
||||||
# Socket errors which need special handling; some of these
|
|
||||||
# might be caused by server API call overload.
|
|
||||||
if (six.text_type(excep).find(ADDRESS_IN_USE_ERROR) != -1 or
|
|
||||||
six.text_type(excep).find(CONN_ABORT_ERROR)) != -1:
|
|
||||||
raise exceptions.VimSessionOverLoadException(
|
|
||||||
_("Socket error in %s.") % attr_name, excep)
|
|
||||||
# Type error which needs special handling; it might be caused
|
|
||||||
# by server API call overload.
|
|
||||||
elif six.text_type(excep).find(RESP_NOT_XML_ERROR) != -1:
|
|
||||||
raise exceptions.VimSessionOverLoadException(
|
|
||||||
_("Type error in %s.") % attr_name, excep)
|
|
||||||
else:
|
|
||||||
raise exceptions.VimException(
|
|
||||||
_("Exception in %s.") % attr_name, excep)
|
|
||||||
return request_handler
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "vSphere object"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "vSphere object"
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,38 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from oslo.vmware import service
|
from oslo_vmware.vim import * # noqa
|
||||||
|
|
||||||
|
|
||||||
class Vim(service.Service):
|
|
||||||
"""Service class that provides access to the VIM API."""
|
|
||||||
|
|
||||||
def __init__(self, protocol='https', host='localhost', port=None,
|
|
||||||
wsdl_url=None, cacert=None, insecure=True):
|
|
||||||
"""Constructs a VIM service client object.
|
|
||||||
|
|
||||||
:param protocol: http or https
|
|
||||||
:param host: server IP address or host name
|
|
||||||
:param port: port for connection
|
|
||||||
:param wsdl_url: VIM WSDL url
|
|
||||||
:param cacert: Specify a CA bundle file to use in verifying a
|
|
||||||
TLS (https) server certificate.
|
|
||||||
:param insecure: Verify HTTPS connections using system certificates,
|
|
||||||
used only if cacert is not specified
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
base_url = service.Service.build_base_url(protocol, host, port)
|
|
||||||
soap_url = base_url + '/sdk'
|
|
||||||
if wsdl_url is None:
|
|
||||||
wsdl_url = soap_url + '/vimService.wsdl'
|
|
||||||
super(Vim, self).__init__(wsdl_url, soap_url, cacert, insecure)
|
|
||||||
|
|
||||||
def retrieve_service_content(self):
|
|
||||||
return self.RetrieveServiceContent(service.SERVICE_INSTANCE)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "VIM Object"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "VIM Object"
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Copyright (c) 2014 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,474 +10,4 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
from oslo_vmware.vim_util import * # noqa
|
||||||
The VMware API utility module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from suds import sudsobject
|
|
||||||
|
|
||||||
from oslo.utils import timeutils
|
|
||||||
|
|
||||||
|
|
||||||
def get_moref(value, type_):
|
|
||||||
"""Get managed object reference.
|
|
||||||
|
|
||||||
:param value: value of the managed object
|
|
||||||
:param type_: type of the managed object
|
|
||||||
:returns: managed object reference with given value and type
|
|
||||||
"""
|
|
||||||
moref = sudsobject.Property(value)
|
|
||||||
moref._type = type_
|
|
||||||
return moref
|
|
||||||
|
|
||||||
|
|
||||||
def build_selection_spec(client_factory, name):
|
|
||||||
"""Builds the selection spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param name: name for the selection spec
|
|
||||||
:returns: selection spec
|
|
||||||
"""
|
|
||||||
sel_spec = client_factory.create('ns0:SelectionSpec')
|
|
||||||
sel_spec.name = name
|
|
||||||
return sel_spec
|
|
||||||
|
|
||||||
|
|
||||||
def build_traversal_spec(client_factory, name, type_, path, skip, select_set):
|
|
||||||
"""Builds the traversal spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param name: name for the traversal spec
|
|
||||||
:param type_: type of the managed object
|
|
||||||
:param path: property path of the managed object
|
|
||||||
:param skip: whether or not to filter the object identified by param path
|
|
||||||
:param select_set: set of selection specs specifying additional objects
|
|
||||||
to filter
|
|
||||||
:returns: traversal spec
|
|
||||||
"""
|
|
||||||
traversal_spec = client_factory.create('ns0:TraversalSpec')
|
|
||||||
traversal_spec.name = name
|
|
||||||
traversal_spec.type = type_
|
|
||||||
traversal_spec.path = path
|
|
||||||
traversal_spec.skip = skip
|
|
||||||
traversal_spec.selectSet = select_set
|
|
||||||
return traversal_spec
|
|
||||||
|
|
||||||
|
|
||||||
def build_recursive_traversal_spec(client_factory):
|
|
||||||
"""Builds recursive traversal spec to traverse managed object hierarchy.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:returns: recursive traversal spec
|
|
||||||
"""
|
|
||||||
visit_folders_select_spec = build_selection_spec(client_factory,
|
|
||||||
'visitFolders')
|
|
||||||
# Next hop from Datacenter
|
|
||||||
dc_to_hf = build_traversal_spec(client_factory,
|
|
||||||
'dc_to_hf',
|
|
||||||
'Datacenter',
|
|
||||||
'hostFolder',
|
|
||||||
False,
|
|
||||||
[visit_folders_select_spec])
|
|
||||||
dc_to_vmf = build_traversal_spec(client_factory,
|
|
||||||
'dc_to_vmf',
|
|
||||||
'Datacenter',
|
|
||||||
'vmFolder',
|
|
||||||
False,
|
|
||||||
[visit_folders_select_spec])
|
|
||||||
dc_to_netf = build_traversal_spec(client_factory,
|
|
||||||
'dc_to_netf',
|
|
||||||
'Datacenter',
|
|
||||||
'networkFolder',
|
|
||||||
False,
|
|
||||||
[visit_folders_select_spec])
|
|
||||||
|
|
||||||
# Next hop from HostSystem
|
|
||||||
h_to_vm = build_traversal_spec(client_factory,
|
|
||||||
'h_to_vm',
|
|
||||||
'HostSystem',
|
|
||||||
'vm',
|
|
||||||
False,
|
|
||||||
[visit_folders_select_spec])
|
|
||||||
|
|
||||||
# Next hop from ComputeResource
|
|
||||||
cr_to_h = build_traversal_spec(client_factory,
|
|
||||||
'cr_to_h',
|
|
||||||
'ComputeResource',
|
|
||||||
'host',
|
|
||||||
False,
|
|
||||||
[])
|
|
||||||
cr_to_ds = build_traversal_spec(client_factory,
|
|
||||||
'cr_to_ds',
|
|
||||||
'ComputeResource',
|
|
||||||
'datastore',
|
|
||||||
False,
|
|
||||||
[])
|
|
||||||
|
|
||||||
rp_to_rp_select_spec = build_selection_spec(client_factory, 'rp_to_rp')
|
|
||||||
rp_to_vm_select_spec = build_selection_spec(client_factory, 'rp_to_vm')
|
|
||||||
|
|
||||||
cr_to_rp = build_traversal_spec(client_factory,
|
|
||||||
'cr_to_rp',
|
|
||||||
'ComputeResource',
|
|
||||||
'resourcePool',
|
|
||||||
False,
|
|
||||||
[rp_to_rp_select_spec,
|
|
||||||
rp_to_vm_select_spec])
|
|
||||||
|
|
||||||
# Next hop from ClusterComputeResource
|
|
||||||
ccr_to_h = build_traversal_spec(client_factory,
|
|
||||||
'ccr_to_h',
|
|
||||||
'ClusterComputeResource',
|
|
||||||
'host',
|
|
||||||
False,
|
|
||||||
[])
|
|
||||||
ccr_to_ds = build_traversal_spec(client_factory,
|
|
||||||
'ccr_to_ds',
|
|
||||||
'ClusterComputeResource',
|
|
||||||
'datastore',
|
|
||||||
False,
|
|
||||||
[])
|
|
||||||
ccr_to_rp = build_traversal_spec(client_factory,
|
|
||||||
'ccr_to_rp',
|
|
||||||
'ClusterComputeResource',
|
|
||||||
'resourcePool',
|
|
||||||
False,
|
|
||||||
[rp_to_rp_select_spec,
|
|
||||||
rp_to_vm_select_spec])
|
|
||||||
# Next hop from ResourcePool
|
|
||||||
rp_to_rp = build_traversal_spec(client_factory,
|
|
||||||
'rp_to_rp',
|
|
||||||
'ResourcePool',
|
|
||||||
'resourcePool',
|
|
||||||
False,
|
|
||||||
[rp_to_rp_select_spec,
|
|
||||||
rp_to_vm_select_spec])
|
|
||||||
rp_to_vm = build_traversal_spec(client_factory,
|
|
||||||
'rp_to_vm',
|
|
||||||
'ResourcePool',
|
|
||||||
'vm',
|
|
||||||
False,
|
|
||||||
[rp_to_rp_select_spec,
|
|
||||||
rp_to_vm_select_spec])
|
|
||||||
|
|
||||||
# Get the assorted traversal spec which takes care of the objects to
|
|
||||||
# be searched for from the rootFolder
|
|
||||||
traversal_spec = build_traversal_spec(client_factory,
|
|
||||||
'visitFolders',
|
|
||||||
'Folder',
|
|
||||||
'childEntity',
|
|
||||||
False,
|
|
||||||
[visit_folders_select_spec,
|
|
||||||
h_to_vm,
|
|
||||||
dc_to_hf,
|
|
||||||
dc_to_vmf,
|
|
||||||
dc_to_netf,
|
|
||||||
cr_to_ds,
|
|
||||||
cr_to_h,
|
|
||||||
cr_to_rp,
|
|
||||||
ccr_to_h,
|
|
||||||
ccr_to_ds,
|
|
||||||
ccr_to_rp,
|
|
||||||
rp_to_rp,
|
|
||||||
rp_to_vm])
|
|
||||||
return traversal_spec
|
|
||||||
|
|
||||||
|
|
||||||
def build_property_spec(client_factory, type_='VirtualMachine',
|
|
||||||
properties_to_collect=None, all_properties=False):
|
|
||||||
"""Builds the property spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param type_: type of the managed object
|
|
||||||
:param properties_to_collect: names of the managed object properties to be
|
|
||||||
collected while traversal filtering
|
|
||||||
:param all_properties: whether all properties of the managed object need
|
|
||||||
to be collected
|
|
||||||
:returns: property spec
|
|
||||||
"""
|
|
||||||
if not properties_to_collect:
|
|
||||||
properties_to_collect = ['name']
|
|
||||||
|
|
||||||
property_spec = client_factory.create('ns0:PropertySpec')
|
|
||||||
property_spec.all = all_properties
|
|
||||||
property_spec.pathSet = properties_to_collect
|
|
||||||
property_spec.type = type_
|
|
||||||
return property_spec
|
|
||||||
|
|
||||||
|
|
||||||
def build_object_spec(client_factory, root_folder, traversal_specs):
|
|
||||||
"""Builds the object spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param root_folder: root folder reference; the starting point of traversal
|
|
||||||
:param traversal_specs: filter specs required for traversal
|
|
||||||
:returns: object spec
|
|
||||||
"""
|
|
||||||
object_spec = client_factory.create('ns0:ObjectSpec')
|
|
||||||
object_spec.obj = root_folder
|
|
||||||
object_spec.skip = False
|
|
||||||
object_spec.selectSet = traversal_specs
|
|
||||||
return object_spec
|
|
||||||
|
|
||||||
|
|
||||||
def build_property_filter_spec(client_factory, property_specs, object_specs):
|
|
||||||
"""Builds the property filter spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param property_specs: property specs to be collected for filtered objects
|
|
||||||
:param object_specs: object specs to identify objects to be filtered
|
|
||||||
:returns: property filter spec
|
|
||||||
"""
|
|
||||||
property_filter_spec = client_factory.create('ns0:PropertyFilterSpec')
|
|
||||||
property_filter_spec.propSet = property_specs
|
|
||||||
property_filter_spec.objectSet = object_specs
|
|
||||||
return property_filter_spec
|
|
||||||
|
|
||||||
|
|
||||||
def get_objects(vim, type_, max_objects, properties_to_collect=None,
|
|
||||||
all_properties=False):
|
|
||||||
"""Get all managed object references of the given type.
|
|
||||||
|
|
||||||
It is the caller's responsibility to continue or cancel retrieval.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param type_: type of the managed object
|
|
||||||
:param max_objects: maximum number of objects that should be returned in
|
|
||||||
a single call
|
|
||||||
:param properties_to_collect: names of the managed object properties to be
|
|
||||||
collected
|
|
||||||
:param all_properties: whether all properties of the managed object need to
|
|
||||||
be collected
|
|
||||||
:returns: all managed object references of the given type
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
if not properties_to_collect:
|
|
||||||
properties_to_collect = ['name']
|
|
||||||
|
|
||||||
client_factory = vim.client.factory
|
|
||||||
recur_trav_spec = build_recursive_traversal_spec(client_factory)
|
|
||||||
object_spec = build_object_spec(client_factory,
|
|
||||||
vim.service_content.rootFolder,
|
|
||||||
[recur_trav_spec])
|
|
||||||
property_spec = build_property_spec(
|
|
||||||
client_factory,
|
|
||||||
type_=type_,
|
|
||||||
properties_to_collect=properties_to_collect,
|
|
||||||
all_properties=all_properties)
|
|
||||||
property_filter_spec = build_property_filter_spec(client_factory,
|
|
||||||
[property_spec],
|
|
||||||
[object_spec])
|
|
||||||
options = client_factory.create('ns0:RetrieveOptions')
|
|
||||||
options.maxObjects = max_objects
|
|
||||||
return vim.RetrievePropertiesEx(vim.service_content.propertyCollector,
|
|
||||||
specSet=[property_filter_spec],
|
|
||||||
options=options)
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_properties(vim, moref, properties_to_collect):
|
|
||||||
"""Get properties of the given managed object.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param moref: managed object reference
|
|
||||||
:param properties_to_collect: names of the managed object properties to be
|
|
||||||
collected
|
|
||||||
:returns: properties of the given managed object
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
if moref is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
client_factory = vim.client.factory
|
|
||||||
all_properties = (properties_to_collect is None or
|
|
||||||
len(properties_to_collect) == 0)
|
|
||||||
property_spec = build_property_spec(
|
|
||||||
client_factory,
|
|
||||||
type_=moref._type,
|
|
||||||
properties_to_collect=properties_to_collect,
|
|
||||||
all_properties=all_properties)
|
|
||||||
object_spec = build_object_spec(client_factory, moref, [])
|
|
||||||
property_filter_spec = build_property_filter_spec(client_factory,
|
|
||||||
[property_spec],
|
|
||||||
[object_spec])
|
|
||||||
|
|
||||||
options = client_factory.create('ns0:RetrieveOptions')
|
|
||||||
options.maxObjects = 1
|
|
||||||
retrieve_result = vim.RetrievePropertiesEx(
|
|
||||||
vim.service_content.propertyCollector,
|
|
||||||
specSet=[property_filter_spec],
|
|
||||||
options=options)
|
|
||||||
cancel_retrieval(vim, retrieve_result)
|
|
||||||
return retrieve_result.objects
|
|
||||||
|
|
||||||
|
|
||||||
def _get_token(retrieve_result):
|
|
||||||
"""Get token from result to obtain next set of results.
|
|
||||||
|
|
||||||
:retrieve_result: Result of RetrievePropertiesEx API call
|
|
||||||
:returns: token to obtain next set of results; None if no more results.
|
|
||||||
"""
|
|
||||||
return getattr(retrieve_result, 'token', None)
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_retrieval(vim, retrieve_result):
|
|
||||||
"""Cancels the retrieve operation if necessary.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param retrieve_result: result of RetrievePropertiesEx API call
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
token = _get_token(retrieve_result)
|
|
||||||
if token:
|
|
||||||
collector = vim.service_content.propertyCollector
|
|
||||||
vim.CancelRetrievePropertiesEx(collector, token=token)
|
|
||||||
|
|
||||||
|
|
||||||
def continue_retrieval(vim, retrieve_result):
|
|
||||||
"""Continue retrieving results, if available.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param retrieve_result: result of RetrievePropertiesEx API call
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
token = _get_token(retrieve_result)
|
|
||||||
if token:
|
|
||||||
collector = vim.service_content.propertyCollector
|
|
||||||
return vim.ContinueRetrievePropertiesEx(collector, token=token)
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_property(vim, moref, property_name):
|
|
||||||
"""Get property of the given managed object.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param moref: managed object reference
|
|
||||||
:param property_name: name of the property to be retrieved
|
|
||||||
:returns: property of the given managed object
|
|
||||||
:raises: VimException, VimFaultException, VimAttributeException,
|
|
||||||
VimSessionOverLoadException, VimConnectionException
|
|
||||||
"""
|
|
||||||
props = get_object_properties(vim, moref, [property_name])
|
|
||||||
prop_val = None
|
|
||||||
if props:
|
|
||||||
prop = None
|
|
||||||
if hasattr(props[0], 'propSet'):
|
|
||||||
# propSet will be set only if the server provides value
|
|
||||||
# for the field
|
|
||||||
prop = props[0].propSet
|
|
||||||
if prop:
|
|
||||||
prop_val = prop[0].val
|
|
||||||
return prop_val
|
|
||||||
|
|
||||||
|
|
||||||
def find_extension(vim, key):
|
|
||||||
"""Looks for an existing extension.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param key: the key to search for
|
|
||||||
:returns: the data object Extension or None
|
|
||||||
"""
|
|
||||||
extension_manager = vim.service_content.extensionManager
|
|
||||||
return vim.client.service.FindExtension(extension_manager, key)
|
|
||||||
|
|
||||||
|
|
||||||
def register_extension(vim, key, type, label='OpenStack',
|
|
||||||
summary='OpenStack services', version='1.0'):
|
|
||||||
"""Create a new extention.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param key: the key for the extension
|
|
||||||
:param type: Managed entity type, as defined by the extension. This
|
|
||||||
matches the type field in the configuration about a
|
|
||||||
virtual machine or vApp
|
|
||||||
:param label: Display label
|
|
||||||
:param summary: Summary description
|
|
||||||
:param version: Extension version number as a dot-separated string
|
|
||||||
"""
|
|
||||||
extension_manager = vim.service_content.extensionManager
|
|
||||||
client_factory = vim.client.factory
|
|
||||||
os_ext = client_factory.create('ns0:Extension')
|
|
||||||
os_ext.key = key
|
|
||||||
entity_info = client_factory.create('ns0:ExtManagedEntityInfo')
|
|
||||||
entity_info.type = type
|
|
||||||
os_ext.managedEntityInfo = [entity_info]
|
|
||||||
os_ext.version = version
|
|
||||||
desc = client_factory.create('ns0:Description')
|
|
||||||
desc.label = label
|
|
||||||
desc.summary = summary
|
|
||||||
os_ext.description = desc
|
|
||||||
os_ext.lastHeartbeatTime = timeutils.strtime()
|
|
||||||
vim.client.service.RegisterExtension(extension_manager, os_ext)
|
|
||||||
|
|
||||||
|
|
||||||
def get_vc_version(session):
|
|
||||||
"""Return the dot-separated vCenter version string. For example, "1.2".
|
|
||||||
|
|
||||||
:param session: vCenter soap session
|
|
||||||
:return: vCenter version
|
|
||||||
"""
|
|
||||||
return session.vim.service_content.about.version
|
|
||||||
|
|
||||||
|
|
||||||
def get_inventory_path(vim, entity_ref, max_objects=100):
|
|
||||||
"""Get the inventory path of a managed entity.
|
|
||||||
|
|
||||||
:param vim: Vim object
|
|
||||||
:param entity_ref: managed entity reference
|
|
||||||
:param max_objects: maximum number of objects that should be returned in
|
|
||||||
a single call
|
|
||||||
:return: inventory path of the entity_ref
|
|
||||||
"""
|
|
||||||
client_factory = vim.client.factory
|
|
||||||
property_collector = vim.service_content.propertyCollector
|
|
||||||
|
|
||||||
prop_spec = build_property_spec(client_factory, 'ManagedEntity',
|
|
||||||
['name', 'parent'])
|
|
||||||
select_set = build_selection_spec(client_factory, 'ParentTraversalSpec')
|
|
||||||
select_set = build_traversal_spec(
|
|
||||||
client_factory, 'ParentTraversalSpec', 'ManagedEntity', 'parent',
|
|
||||||
False, [select_set])
|
|
||||||
obj_spec = build_object_spec(client_factory, entity_ref, select_set)
|
|
||||||
prop_filter_spec = build_property_filter_spec(client_factory,
|
|
||||||
[prop_spec], [obj_spec])
|
|
||||||
options = client_factory.create('ns0:RetrieveOptions')
|
|
||||||
options.maxObjects = max_objects
|
|
||||||
retrieve_result = vim.RetrievePropertiesEx(
|
|
||||||
property_collector,
|
|
||||||
specSet=[prop_filter_spec],
|
|
||||||
options=options)
|
|
||||||
entity_name = None
|
|
||||||
propSet = None
|
|
||||||
path = ""
|
|
||||||
while retrieve_result:
|
|
||||||
for obj in retrieve_result.objects:
|
|
||||||
if hasattr(obj, 'propSet'):
|
|
||||||
propSet = obj.propSet
|
|
||||||
if len(propSet) >= 1 and not entity_name:
|
|
||||||
entity_name = propSet[0].val
|
|
||||||
elif len(propSet) >= 1:
|
|
||||||
path = '%s/%s' % (propSet[0].val, path)
|
|
||||||
retrieve_result = continue_retrieval(vim, retrieve_result)
|
|
||||||
# NOTE(arnaud): slice to exclude the root folder from the result.
|
|
||||||
if propSet is not None and len(propSet) > 0:
|
|
||||||
path = path[len(propSet[0].val):]
|
|
||||||
if entity_name is None:
|
|
||||||
entity_name = ""
|
|
||||||
return '%s%s' % (path, entity_name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_http_service_request_spec(client_factory, method, uri):
|
|
||||||
"""Build a HTTP service request spec.
|
|
||||||
|
|
||||||
:param client_factory: factory to get API input specs
|
|
||||||
:param method: HTTP method (GET, POST, PUT)
|
|
||||||
:param uri: target URL
|
|
||||||
"""
|
|
||||||
http_service_request_spec = client_factory.create(
|
|
||||||
'ns0:SessionManagerHttpServiceRequestSpec')
|
|
||||||
http_service_request_spec.method = method
|
|
||||||
http_service_request_spec.url = uri
|
|
||||||
return http_service_request_spec
|
|
||||||
|
500
oslo_vmware/api.py
Normal file
500
oslo_vmware/api.py
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Session and API call management for VMware ESX/VC server.
|
||||||
|
|
||||||
|
This module contains classes to invoke VIM APIs. It supports
|
||||||
|
automatic session re-establishment and retry of API invocations
|
||||||
|
in case of connection problems or server API call overload.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo.utils import excutils
|
||||||
|
from oslo_vmware._i18n import _, _LE, _LI, _LW
|
||||||
|
from oslo_vmware.common import loopingcall
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import pbm
|
||||||
|
from oslo_vmware import vim
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _trunc_id(session_id):
|
||||||
|
"""Returns truncated session id which is suitable for logging."""
|
||||||
|
if session_id is not None:
|
||||||
|
return session_id[-5:]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(vbala) Move this class to excutils.py.
|
||||||
|
class RetryDecorator(object):
|
||||||
|
"""Decorator for retrying a function upon suggested exceptions.
|
||||||
|
|
||||||
|
The decorated function is retried for the given number of times, and the
|
||||||
|
sleep time between the retries is incremented until max sleep time is
|
||||||
|
reached. If the max retry count is set to -1, then the decorated function
|
||||||
|
is invoked indefinitely until an exception is thrown, and the caught
|
||||||
|
exception is not in the list of suggested exceptions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_retry_count=-1, inc_sleep_time=10,
|
||||||
|
max_sleep_time=60, exceptions=()):
|
||||||
|
"""Configure the retry object using the input params.
|
||||||
|
|
||||||
|
:param max_retry_count: maximum number of times the given function must
|
||||||
|
be retried when one of the input 'exceptions'
|
||||||
|
is caught. When set to -1, it will be retried
|
||||||
|
indefinitely until an exception is thrown
|
||||||
|
and the caught exception is not in param
|
||||||
|
exceptions.
|
||||||
|
:param inc_sleep_time: incremental time in seconds for sleep time
|
||||||
|
between retries
|
||||||
|
:param max_sleep_time: max sleep time in seconds beyond which the sleep
|
||||||
|
time will not be incremented using param
|
||||||
|
inc_sleep_time. On reaching this threshold,
|
||||||
|
max_sleep_time will be used as the sleep time.
|
||||||
|
:param exceptions: suggested exceptions for which the function must be
|
||||||
|
retried
|
||||||
|
"""
|
||||||
|
self._max_retry_count = max_retry_count
|
||||||
|
self._inc_sleep_time = inc_sleep_time
|
||||||
|
self._max_sleep_time = max_sleep_time
|
||||||
|
self._exceptions = exceptions
|
||||||
|
self._retry_count = 0
|
||||||
|
self._sleep_time = 0
|
||||||
|
|
||||||
|
def __call__(self, f):
|
||||||
|
|
||||||
|
def _func(*args, **kwargs):
|
||||||
|
func_name = f.__name__
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
if self._retry_count:
|
||||||
|
LOG.debug("Invoking %(func_name)s; retry count is "
|
||||||
|
"%(retry_count)d.",
|
||||||
|
{'func_name': func_name,
|
||||||
|
'retry_count': self._retry_count})
|
||||||
|
result = f(*args, **kwargs)
|
||||||
|
except self._exceptions:
|
||||||
|
with excutils.save_and_reraise_exception() as ctxt:
|
||||||
|
LOG.warn(_LW("Exception which is in the suggested list of "
|
||||||
|
"exceptions occurred while invoking function:"
|
||||||
|
" %s."),
|
||||||
|
func_name,
|
||||||
|
exc_info=True)
|
||||||
|
if (self._max_retry_count != -1 and
|
||||||
|
self._retry_count >= self._max_retry_count):
|
||||||
|
LOG.error(_LE("Cannot retry upon suggested exception "
|
||||||
|
"since retry count (%(retry_count)d) "
|
||||||
|
"reached max retry count "
|
||||||
|
"(%(max_retry_count)d)."),
|
||||||
|
{'retry_count': self._retry_count,
|
||||||
|
'max_retry_count': self._max_retry_count})
|
||||||
|
else:
|
||||||
|
ctxt.reraise = False
|
||||||
|
self._retry_count += 1
|
||||||
|
self._sleep_time += self._inc_sleep_time
|
||||||
|
return self._sleep_time
|
||||||
|
raise loopingcall.LoopingCallDone(result)
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
loop = loopingcall.DynamicLoopingCall(_func, *args, **kwargs)
|
||||||
|
evt = loop.start(periodic_interval_max=self._max_sleep_time)
|
||||||
|
LOG.debug("Waiting for function %s to return.", f.__name__)
|
||||||
|
return evt.wait()
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class VMwareAPISession(object):
|
||||||
|
"""Setup a session with the server and handles all calls made to it.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
api_session = VMwareAPISession('10.1.2.3', 'administrator',
|
||||||
|
'password', 10, 0.1,
|
||||||
|
create_session=False, port=443)
|
||||||
|
result = api_session.invoke_api(vim_util, 'get_objects',
|
||||||
|
api_session.vim, 'HostSystem', 100)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host, server_username, server_password,
|
||||||
|
api_retry_count, task_poll_interval, scheme='https',
|
||||||
|
create_session=True, wsdl_loc=None, pbm_wsdl_loc=None,
|
||||||
|
port=443, cacert=None, insecure=True):
|
||||||
|
"""Initializes the API session with given parameters.
|
||||||
|
|
||||||
|
:param host: ESX/VC server IP address or host name
|
||||||
|
:param port: port for connection
|
||||||
|
:param server_username: username of ESX/VC server admin user
|
||||||
|
:param server_password: password for param server_username
|
||||||
|
:param api_retry_count: number of times an API must be retried upon
|
||||||
|
session/connection related errors
|
||||||
|
:param task_poll_interval: sleep time in seconds for polling an
|
||||||
|
on-going async task as part of the API call
|
||||||
|
:param scheme: protocol-- http or https
|
||||||
|
:param create_session: whether to setup a connection at the time of
|
||||||
|
instance creation
|
||||||
|
:param wsdl_loc: VIM API WSDL file location
|
||||||
|
:param pbm_wsdl_loc: PBM service WSDL file location
|
||||||
|
:param cacert: Specify a CA bundle file to use in verifying a
|
||||||
|
TLS (https) server certificate.
|
||||||
|
:param insecure: Verify HTTPS connections using system certificates,
|
||||||
|
used only if cacert is not specified
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException
|
||||||
|
"""
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._server_username = server_username
|
||||||
|
self._server_password = server_password
|
||||||
|
self._api_retry_count = api_retry_count
|
||||||
|
self._task_poll_interval = task_poll_interval
|
||||||
|
self._scheme = scheme
|
||||||
|
self._vim_wsdl_loc = wsdl_loc
|
||||||
|
self._pbm_wsdl_loc = pbm_wsdl_loc
|
||||||
|
self._session_id = None
|
||||||
|
self._session_username = None
|
||||||
|
self._vim = None
|
||||||
|
self._pbm = None
|
||||||
|
self._cacert = cacert
|
||||||
|
self._insecure = insecure
|
||||||
|
if create_session:
|
||||||
|
self._create_session()
|
||||||
|
|
||||||
|
def pbm_wsdl_loc_set(self, pbm_wsdl_loc):
|
||||||
|
self._pbm_wsdl_loc = pbm_wsdl_loc
|
||||||
|
self._pbm = None
|
||||||
|
LOG.info(_LI('PBM WSDL updated to %s'), pbm_wsdl_loc)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vim(self):
|
||||||
|
if not self._vim:
|
||||||
|
self._vim = vim.Vim(protocol=self._scheme,
|
||||||
|
host=self._host,
|
||||||
|
port=self._port,
|
||||||
|
wsdl_url=self._vim_wsdl_loc,
|
||||||
|
cacert=self._cacert,
|
||||||
|
insecure=self._insecure)
|
||||||
|
return self._vim
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pbm(self):
|
||||||
|
if not self._pbm and self._pbm_wsdl_loc:
|
||||||
|
self._pbm = pbm.Pbm(protocol=self._scheme,
|
||||||
|
host=self._host,
|
||||||
|
port=self._port,
|
||||||
|
wsdl_url=self._pbm_wsdl_loc,
|
||||||
|
cacert=self._cacert,
|
||||||
|
insecure=self._insecure)
|
||||||
|
if self._session_id:
|
||||||
|
# To handle the case where pbm property is accessed after
|
||||||
|
# session creation. If pbm property is accessed before session
|
||||||
|
# creation, we set the cookie in _create_session.
|
||||||
|
self._pbm.set_soap_cookie(self._vim.get_http_cookie())
|
||||||
|
return self._pbm
|
||||||
|
|
||||||
|
@RetryDecorator(exceptions=(exceptions.VimConnectionException,))
|
||||||
|
def _create_session(self):
|
||||||
|
"""Establish session with the server."""
|
||||||
|
session_manager = self.vim.service_content.sessionManager
|
||||||
|
# Login and create new session with the server for making API calls.
|
||||||
|
LOG.debug("Logging in with username = %s.", self._server_username)
|
||||||
|
session = self.vim.Login(session_manager,
|
||||||
|
userName=self._server_username,
|
||||||
|
password=self._server_password)
|
||||||
|
prev_session_id, self._session_id = self._session_id, session.key
|
||||||
|
# We need to save the username in the session since we may need it
|
||||||
|
# later to check active session. The SessionIsActive method requires
|
||||||
|
# the username parameter to be exactly same as that in the session
|
||||||
|
# object. We can't use the username used for login since the Login
|
||||||
|
# method ignores the case.
|
||||||
|
self._session_username = session.userName
|
||||||
|
LOG.info(_LI("Successfully established new session; session ID is "
|
||||||
|
"%s."),
|
||||||
|
_trunc_id(self._session_id))
|
||||||
|
|
||||||
|
# Terminate the previous session (if exists) for preserving sessions
|
||||||
|
# as there is a limit on the number of sessions we can have.
|
||||||
|
if prev_session_id:
|
||||||
|
try:
|
||||||
|
LOG.info(_LI("Terminating the previous session with ID = %s"),
|
||||||
|
_trunc_id(prev_session_id))
|
||||||
|
self.vim.TerminateSession(session_manager,
|
||||||
|
sessionId=[prev_session_id])
|
||||||
|
except Exception:
|
||||||
|
# This exception is something we can live with. It is
|
||||||
|
# just an extra caution on our side. The session might
|
||||||
|
# have been cleared already. We could have made a call to
|
||||||
|
# SessionIsActive, but that is an overhead because we
|
||||||
|
# anyway would have to call TerminateSession.
|
||||||
|
LOG.warn(_LW("Error occurred while terminating the previous "
|
||||||
|
"session with ID = %s."),
|
||||||
|
_trunc_id(prev_session_id),
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
|
# Set PBM client cookie.
|
||||||
|
if self._pbm is not None:
|
||||||
|
self._pbm.set_soap_cookie(self._vim.get_http_cookie())
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
"""Log out and terminate the current session."""
|
||||||
|
if self._session_id:
|
||||||
|
LOG.info(_LI("Logging out and terminating the current session "
|
||||||
|
"with ID = %s."),
|
||||||
|
_trunc_id(self._session_id))
|
||||||
|
try:
|
||||||
|
self.vim.Logout(self.vim.service_content.sessionManager)
|
||||||
|
self._session_id = None
|
||||||
|
except Exception:
|
||||||
|
LOG.exception(_LE("Error occurred while logging out and "
|
||||||
|
"terminating the current session with "
|
||||||
|
"ID = %s."),
|
||||||
|
_trunc_id(self._session_id))
|
||||||
|
else:
|
||||||
|
LOG.debug("No session exists to log out.")
|
||||||
|
|
||||||
|
def invoke_api(self, module, method, *args, **kwargs):
|
||||||
|
"""Wrapper method for invoking APIs.
|
||||||
|
|
||||||
|
The API call is retried in the event of exceptions due to session
|
||||||
|
overload or connection problems.
|
||||||
|
|
||||||
|
:param module: module corresponding to the VIM API call
|
||||||
|
:param method: method in the module which corresponds to the
|
||||||
|
VIM API call
|
||||||
|
:param args: arguments to the method
|
||||||
|
:param kwargs: keyword arguments to the method
|
||||||
|
:returns: response from the API call
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
|
||||||
|
@RetryDecorator(max_retry_count=self._api_retry_count,
|
||||||
|
exceptions=(exceptions.VimSessionOverLoadException,
|
||||||
|
exceptions.VimConnectionException))
|
||||||
|
def _invoke_api(module, method, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
api_method = getattr(module, method)
|
||||||
|
return api_method(*args, **kwargs)
|
||||||
|
except exceptions.VimFaultException as excep:
|
||||||
|
# If this is due to an inactive session, we should re-create
|
||||||
|
# the session and retry.
|
||||||
|
if exceptions.NOT_AUTHENTICATED in excep.fault_list:
|
||||||
|
# The NotAuthenticated fault is set by the fault checker
|
||||||
|
# due to an empty response. An empty response could be a
|
||||||
|
# valid response; for e.g., response for the query to
|
||||||
|
# return the VMs in an ESX server which has no VMs in it.
|
||||||
|
# Also, the server responds with an empty response in the
|
||||||
|
# case of an inactive session. Therefore, we need a way to
|
||||||
|
# differentiate between these two cases.
|
||||||
|
if self.is_current_session_active():
|
||||||
|
LOG.debug("Returning empty response for "
|
||||||
|
"%(module)s.%(method)s invocation.",
|
||||||
|
{'module': module,
|
||||||
|
'method': method})
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
# empty response is due to an inactive session
|
||||||
|
excep_msg = (
|
||||||
|
_("Current session: %(session)s is inactive; "
|
||||||
|
"re-creating the session while invoking "
|
||||||
|
"method %(module)s.%(method)s.") %
|
||||||
|
{'session': _trunc_id(self._session_id),
|
||||||
|
'module': module,
|
||||||
|
'method': method})
|
||||||
|
LOG.warn(excep_msg, exc_info=True)
|
||||||
|
self._create_session()
|
||||||
|
raise exceptions.VimConnectionException(excep_msg,
|
||||||
|
excep)
|
||||||
|
else:
|
||||||
|
# no need to retry for other VIM faults like
|
||||||
|
# InvalidArgument
|
||||||
|
# Raise specific exceptions here if possible
|
||||||
|
if excep.fault_list:
|
||||||
|
LOG.debug("Fault list: %s", excep.fault_list)
|
||||||
|
fault = excep.fault_list[0]
|
||||||
|
clazz = exceptions.get_fault_class(fault)
|
||||||
|
raise clazz(six.text_type(excep), excep.details)
|
||||||
|
raise
|
||||||
|
|
||||||
|
except exceptions.VimConnectionException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Re-create the session during connection exception only
|
||||||
|
# if the session has expired. Otherwise, it could be
|
||||||
|
# a transient issue.
|
||||||
|
if not self.is_current_session_active():
|
||||||
|
LOG.warn(_LW("Re-creating session due to connection "
|
||||||
|
"problems while invoking method "
|
||||||
|
"%(module)s.%(method)s."),
|
||||||
|
{'module': module,
|
||||||
|
'method': method},
|
||||||
|
exc_info=True)
|
||||||
|
self._create_session()
|
||||||
|
|
||||||
|
return _invoke_api(module, method, *args, **kwargs)
|
||||||
|
|
||||||
|
def is_current_session_active(self):
|
||||||
|
"""Check if current session is active.
|
||||||
|
|
||||||
|
:returns: True if the session is active; False otherwise
|
||||||
|
"""
|
||||||
|
LOG.debug("Checking if the current session: %s is active.",
|
||||||
|
_trunc_id(self._session_id))
|
||||||
|
|
||||||
|
is_active = False
|
||||||
|
try:
|
||||||
|
is_active = self.vim.SessionIsActive(
|
||||||
|
self.vim.service_content.sessionManager,
|
||||||
|
sessionID=self._session_id,
|
||||||
|
userName=self._session_username)
|
||||||
|
except exceptions.VimException:
|
||||||
|
LOG.warn(_LW("Error occurred while checking whether the "
|
||||||
|
"current session: %s is active."),
|
||||||
|
_trunc_id(self._session_id),
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
|
return is_active
|
||||||
|
|
||||||
|
def wait_for_task(self, task):
|
||||||
|
"""Waits for the given task to complete and returns the result.
|
||||||
|
|
||||||
|
The task is polled until it is done. The method returns the task
|
||||||
|
information upon successful completion. In case of any error,
|
||||||
|
appropriate exception is raised.
|
||||||
|
|
||||||
|
:param task: managed object reference of the task
|
||||||
|
:returns: task info upon successful completion of the task
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
loop = loopingcall.FixedIntervalLoopingCall(self._poll_task, task)
|
||||||
|
evt = loop.start(self._task_poll_interval)
|
||||||
|
LOG.debug("Waiting for the task: %s to complete.", task)
|
||||||
|
return evt.wait()
|
||||||
|
|
||||||
|
def _poll_task(self, task):
|
||||||
|
"""Poll the given task until completion.
|
||||||
|
|
||||||
|
If the task completes successfully, the method returns the task info
|
||||||
|
using the input event (param done). In case of any error, appropriate
|
||||||
|
exception is set in the event.
|
||||||
|
|
||||||
|
:param task: managed object reference of the task
|
||||||
|
"""
|
||||||
|
LOG.debug("Invoking VIM API to read info of task: %s.", task)
|
||||||
|
try:
|
||||||
|
task_info = self.invoke_api(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
self.vim,
|
||||||
|
task,
|
||||||
|
'info')
|
||||||
|
except exceptions.VimException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Error occurred while reading info of "
|
||||||
|
"task: %s."),
|
||||||
|
task)
|
||||||
|
else:
|
||||||
|
if task_info.state in ['queued', 'running']:
|
||||||
|
if hasattr(task_info, 'progress'):
|
||||||
|
LOG.debug("Task: %(task)s progress is %(progress)s%%.",
|
||||||
|
{'task': task,
|
||||||
|
'progress': task_info.progress})
|
||||||
|
elif task_info.state == 'success':
|
||||||
|
LOG.debug("Task: %s status is success.", task)
|
||||||
|
raise loopingcall.LoopingCallDone(task_info)
|
||||||
|
else:
|
||||||
|
error_msg = six.text_type(task_info.error.localizedMessage)
|
||||||
|
error = task_info.error
|
||||||
|
name = error.fault.__class__.__name__
|
||||||
|
task_ex = exceptions.get_fault_class(name)(error_msg)
|
||||||
|
raise task_ex
|
||||||
|
|
||||||
|
def wait_for_lease_ready(self, lease):
|
||||||
|
"""Waits for the given lease to be ready.
|
||||||
|
|
||||||
|
This method return when the lease is ready. In case of any error,
|
||||||
|
appropriate exception is raised.
|
||||||
|
|
||||||
|
:param lease: lease to be checked for
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
loop = loopingcall.FixedIntervalLoopingCall(self._poll_lease, lease)
|
||||||
|
evt = loop.start(self._task_poll_interval)
|
||||||
|
LOG.debug("Waiting for the lease: %s to be ready.", lease)
|
||||||
|
evt.wait()
|
||||||
|
|
||||||
|
def _poll_lease(self, lease):
|
||||||
|
"""Poll the state of the given lease.
|
||||||
|
|
||||||
|
When the lease is ready, the event (param done) is notified. In case
|
||||||
|
of any error, appropriate exception is set in the event.
|
||||||
|
|
||||||
|
:param lease: lease whose state is to be polled
|
||||||
|
"""
|
||||||
|
LOG.debug("Invoking VIM API to read state of lease: %s.", lease)
|
||||||
|
try:
|
||||||
|
state = self.invoke_api(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
self.vim,
|
||||||
|
lease,
|
||||||
|
'state')
|
||||||
|
except exceptions.VimException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Error occurred while checking "
|
||||||
|
"state of lease: %s."),
|
||||||
|
lease)
|
||||||
|
else:
|
||||||
|
if state == 'ready':
|
||||||
|
LOG.debug("Lease: %s is ready.", lease)
|
||||||
|
raise loopingcall.LoopingCallDone()
|
||||||
|
elif state == 'initializing':
|
||||||
|
LOG.debug("Lease: %s is initializing.", lease)
|
||||||
|
elif state == 'error':
|
||||||
|
LOG.debug("Invoking VIM API to read lease: %s error.",
|
||||||
|
lease)
|
||||||
|
error_msg = self._get_error_message(lease)
|
||||||
|
excep_msg = _("Lease: %(lease)s is in error state. Details: "
|
||||||
|
"%(error_msg)s.") % {'lease': lease,
|
||||||
|
'error_msg': error_msg}
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
raise exceptions.VimException(excep_msg)
|
||||||
|
else:
|
||||||
|
# unknown state
|
||||||
|
excep_msg = _("Unknown state: %(state)s for lease: "
|
||||||
|
"%(lease)s.") % {'state': state,
|
||||||
|
'lease': lease}
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
raise exceptions.VimException(excep_msg)
|
||||||
|
|
||||||
|
def _get_error_message(self, lease):
|
||||||
|
"""Get error message associated with the given lease."""
|
||||||
|
try:
|
||||||
|
return self.invoke_api(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
self.vim,
|
||||||
|
lease,
|
||||||
|
'error')
|
||||||
|
except exceptions.VimException:
|
||||||
|
LOG.warn(_LW("Error occurred while reading error message for "
|
||||||
|
"lease: %s."),
|
||||||
|
lease,
|
||||||
|
exc_info=True)
|
||||||
|
return "Unknown"
|
0
oslo_vmware/common/__init__.py
Normal file
0
oslo_vmware/common/__init__.py
Normal file
@ -22,7 +22,7 @@ from eventlet import event
|
|||||||
from eventlet import greenthread
|
from eventlet import greenthread
|
||||||
|
|
||||||
from oslo.utils import timeutils
|
from oslo.utils import timeutils
|
||||||
from oslo.vmware._i18n import _LE, _LW
|
from oslo_vmware._i18n import _LE, _LW
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
32
oslo_vmware/constants.py
Normal file
32
oslo_vmware/constants.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Shared constants across the VMware ecosystem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Datacenter path for HTTP access to datastores if the target server is an ESX/
|
||||||
|
# ESXi system: http://goo.gl/B5Htr8 for more information.
|
||||||
|
ESX_DATACENTER_PATH = 'ha-datacenter'
|
||||||
|
|
||||||
|
# User Agent for HTTP requests between OpenStack and vCenter.
|
||||||
|
USER_AGENT = 'OpenStack-ESX-Adapter'
|
||||||
|
|
||||||
|
# Key of the cookie header when using a SOAP session.
|
||||||
|
SOAP_COOKIE_KEY = 'vmware_soap_session'
|
||||||
|
|
||||||
|
# Key of the cookie header when using a CGI session.
|
||||||
|
CGI_COOKIE_KEY = 'vmware_cgi_ticket'
|
261
oslo_vmware/exceptions.py
Normal file
261
oslo_vmware/exceptions.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exception definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _, _LE
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ALREADY_EXISTS = 'AlreadyExists'
|
||||||
|
CANNOT_DELETE_FILE = 'CannotDeleteFile'
|
||||||
|
FILE_ALREADY_EXISTS = 'FileAlreadyExists'
|
||||||
|
FILE_FAULT = 'FileFault'
|
||||||
|
FILE_LOCKED = 'FileLocked'
|
||||||
|
FILE_NOT_FOUND = 'FileNotFound'
|
||||||
|
INVALID_POWER_STATE = 'InvalidPowerState'
|
||||||
|
INVALID_PROPERTY = 'InvalidProperty'
|
||||||
|
NO_PERMISSION = 'NoPermission'
|
||||||
|
NOT_AUTHENTICATED = 'NotAuthenticated'
|
||||||
|
TASK_IN_PROGRESS = 'TaskInProgress'
|
||||||
|
DUPLICATE_NAME = 'DuplicateName'
|
||||||
|
|
||||||
|
|
||||||
|
class VimException(Exception):
|
||||||
|
"""The base exception class for all exceptions this library raises."""
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
__str__ = lambda self: six.text_type(self).encode('utf8')
|
||||||
|
__unicode__ = lambda self: self.description
|
||||||
|
else:
|
||||||
|
__str__ = lambda self: self.description
|
||||||
|
|
||||||
|
def __init__(self, message, cause=None):
|
||||||
|
Exception.__init__(self)
|
||||||
|
if isinstance(message, list):
|
||||||
|
# we need this to protect against developers using
|
||||||
|
# this method like VimFaultException
|
||||||
|
raise ValueError(_("exception_summary must not be a list"))
|
||||||
|
|
||||||
|
self.msg = message
|
||||||
|
self.cause = cause
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
# NOTE(jecarey): self.msg and self.cause may be i18n objects
|
||||||
|
# that do not support str or concatenation, but can be used
|
||||||
|
# as replacement text.
|
||||||
|
descr = six.text_type(self.msg)
|
||||||
|
if self.cause:
|
||||||
|
descr += '\nCause: ' + six.text_type(self.cause)
|
||||||
|
return descr
|
||||||
|
|
||||||
|
|
||||||
|
class VimSessionOverLoadException(VimException):
|
||||||
|
"""Thrown when there is an API call overload at the VMware server."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VimConnectionException(VimException):
|
||||||
|
"""Thrown when there is a connection problem."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VimAttributeException(VimException):
|
||||||
|
"""Thrown when a particular attribute cannot be found."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VimFaultException(VimException):
|
||||||
|
"""Exception thrown when there are faults during VIM API calls."""
|
||||||
|
|
||||||
|
def __init__(self, fault_list, message, cause=None, details=None):
|
||||||
|
super(VimFaultException, self).__init__(message, cause)
|
||||||
|
if not isinstance(fault_list, list):
|
||||||
|
raise ValueError(_("fault_list must be a list"))
|
||||||
|
if details is not None and not isinstance(details, dict):
|
||||||
|
raise ValueError(_("details must be a dict"))
|
||||||
|
self.fault_list = fault_list
|
||||||
|
self.details = details
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
__unicode__ = lambda self: self.description
|
||||||
|
else:
|
||||||
|
__str__ = lambda self: self.description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
descr = VimException.description.fget(self)
|
||||||
|
if self.fault_list:
|
||||||
|
# fault_list doesn't contain non-ASCII chars, we can use str()
|
||||||
|
descr += '\nFaults: ' + str(self.fault_list)
|
||||||
|
if self.details:
|
||||||
|
# details may contain non-ASCII values
|
||||||
|
details = '{%s}' % ', '.join(["'%s': '%s'" % (k, v) for k, v in
|
||||||
|
six.iteritems(self.details)])
|
||||||
|
descr += '\nDetails: ' + details
|
||||||
|
return descr
|
||||||
|
|
||||||
|
|
||||||
|
class ImageTransferException(VimException):
|
||||||
|
"""Thrown when there is an error during image transfer."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VMwareDriverException(Exception):
|
||||||
|
"""Base VMware Driver Exception
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define
|
||||||
|
a 'msg_fmt' property. That msg_fmt will get printf'd
|
||||||
|
with the keyword arguments provided to the constructor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
msg_fmt = _("An unknown exception occurred.")
|
||||||
|
|
||||||
|
def __init__(self, message=None, details=None, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.details = details
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
try:
|
||||||
|
message = self.msg_fmt % kwargs
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# kwargs doesn't match a variable in the message
|
||||||
|
# log the issue and the kwargs
|
||||||
|
LOG.exception(_LE('Exception in string format operation'))
|
||||||
|
for name, value in six.iteritems(kwargs):
|
||||||
|
LOG.error(_LE("%(name)s: %(value)s"),
|
||||||
|
{'name': name, 'value': value})
|
||||||
|
# at least get the core message out if something happened
|
||||||
|
message = self.msg_fmt
|
||||||
|
|
||||||
|
super(VMwareDriverException, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class VMwareDriverConfigurationException(VMwareDriverException):
|
||||||
|
"""Base class for all configuration exceptions.
|
||||||
|
"""
|
||||||
|
msg_fmt = _("VMware Driver configuration fault.")
|
||||||
|
|
||||||
|
|
||||||
|
class UseLinkedCloneConfigurationFault(VMwareDriverConfigurationException):
|
||||||
|
msg_fmt = _("No default value for use_linked_clone found.")
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParameter(VMwareDriverException):
|
||||||
|
msg_fmt = _("Missing parameter : %(param)s")
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyExistsException(VMwareDriverException):
|
||||||
|
msg_fmt = _("Resource already exists.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class CannotDeleteFileException(VMwareDriverException):
|
||||||
|
msg_fmt = _("Cannot delete file.")
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class FileAlreadyExistsException(VMwareDriverException):
|
||||||
|
msg_fmt = _("File already exists.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class FileFaultException(VMwareDriverException):
|
||||||
|
msg_fmt = _("File fault.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class FileLockedException(VMwareDriverException):
|
||||||
|
msg_fmt = _("File locked.")
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotFoundException(VMwareDriverException):
|
||||||
|
msg_fmt = _("File not found.")
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPowerStateException(VMwareDriverException):
|
||||||
|
msg_fmt = _("Invalid power state.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPropertyException(VMwareDriverException):
|
||||||
|
msg_fmt = _("Invalid property.")
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class NoPermissionException(VMwareDriverException):
|
||||||
|
msg_fmt = _("No Permission.")
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthenticatedException(VMwareDriverException):
|
||||||
|
msg_fmt = _("Not Authenticated.")
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class TaskInProgress(VMwareDriverException):
|
||||||
|
msg_fmt = _("Entity has another operation in process.")
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateName(VMwareDriverException):
|
||||||
|
msg_fmt = _("Duplicate name.")
|
||||||
|
|
||||||
|
|
||||||
|
# Populate the fault registry with the exceptions that have
|
||||||
|
# special treatment.
|
||||||
|
_fault_classes_registry = {
|
||||||
|
ALREADY_EXISTS: AlreadyExistsException,
|
||||||
|
CANNOT_DELETE_FILE: CannotDeleteFileException,
|
||||||
|
FILE_ALREADY_EXISTS: FileAlreadyExistsException,
|
||||||
|
FILE_FAULT: FileFaultException,
|
||||||
|
FILE_LOCKED: FileLockedException,
|
||||||
|
FILE_NOT_FOUND: FileNotFoundException,
|
||||||
|
INVALID_POWER_STATE: InvalidPowerStateException,
|
||||||
|
INVALID_PROPERTY: InvalidPropertyException,
|
||||||
|
NO_PERMISSION: NoPermissionException,
|
||||||
|
NOT_AUTHENTICATED: NotAuthenticatedException,
|
||||||
|
TASK_IN_PROGRESS: TaskInProgress,
|
||||||
|
DUPLICATE_NAME: DuplicateName,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_fault_class(name):
|
||||||
|
"""Get a named subclass of VMwareDriverException."""
|
||||||
|
name = str(name)
|
||||||
|
fault_class = _fault_classes_registry.get(name)
|
||||||
|
if not fault_class:
|
||||||
|
LOG.debug('Fault %s not matched.', name)
|
||||||
|
fault_class = VMwareDriverException
|
||||||
|
return fault_class
|
||||||
|
|
||||||
|
|
||||||
|
def register_fault_class(name, exception):
|
||||||
|
fault_class = _fault_classes_registry.get(name)
|
||||||
|
if not issubclass(exception, VMwareDriverException):
|
||||||
|
raise TypeError(_("exception should be a subclass of "
|
||||||
|
"VMwareDriverException"))
|
||||||
|
if fault_class:
|
||||||
|
LOG.debug('Overriding exception for %s', name)
|
||||||
|
_fault_classes_registry[name] = exception
|
608
oslo_vmware/image_transfer.py
Normal file
608
oslo_vmware/image_transfer.py
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Functions and classes for image transfer between ESX/VC & image service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from eventlet import event
|
||||||
|
from eventlet import greenthread
|
||||||
|
from eventlet import queue
|
||||||
|
from eventlet import timeout
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
|
from oslo_vmware import constants
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware.objects import datastore as ds_obj
|
||||||
|
from oslo_vmware import rw_handles
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
IMAGE_SERVICE_POLL_INTERVAL = 5
|
||||||
|
FILE_READ_WRITE_TASK_SLEEP_TIME = 0.01
|
||||||
|
BLOCKING_QUEUE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
|
class BlockingQueue(queue.LightQueue):
|
||||||
|
"""Producer-Consumer queue to share data between reader/writer threads."""
|
||||||
|
|
||||||
|
def __init__(self, max_size, max_transfer_size):
|
||||||
|
"""Initializes the queue with the given parameters.
|
||||||
|
|
||||||
|
:param max_size: maximum queue size; if max_size is less than zero or
|
||||||
|
None, the queue size is infinite.
|
||||||
|
:param max_transfer_size: maximum amount of data that can be
|
||||||
|
_transferred using this queue
|
||||||
|
"""
|
||||||
|
queue.LightQueue.__init__(self, max_size)
|
||||||
|
self._max_transfer_size = max_transfer_size
|
||||||
|
self._transferred = 0
|
||||||
|
|
||||||
|
def read(self, chunk_size):
|
||||||
|
"""Read data from the queue.
|
||||||
|
|
||||||
|
This method blocks until data is available. The input chunk size is
|
||||||
|
ignored since we have ensured that the data chunks written to the pipe
|
||||||
|
by the image reader thread is the same as the chunks asked for by the
|
||||||
|
image writer thread.
|
||||||
|
"""
|
||||||
|
if (self._max_transfer_size is 0 or
|
||||||
|
self._transferred < self._max_transfer_size):
|
||||||
|
data_item = self.get()
|
||||||
|
self._transferred += len(data_item)
|
||||||
|
return data_item
|
||||||
|
else:
|
||||||
|
LOG.debug("Completed transfer of size %s.", self._transferred)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Write data into the queue.
|
||||||
|
|
||||||
|
:param data: data to be written
|
||||||
|
"""
|
||||||
|
self.put(data)
|
||||||
|
|
||||||
|
# Below methods are provided in order to enable treating the queue
|
||||||
|
# as a file handle.
|
||||||
|
|
||||||
|
def seek(self, offset, whence=0):
|
||||||
|
"""Set the file's current position at the offset.
|
||||||
|
|
||||||
|
This method throws IOError since seek cannot be supported for a pipe.
|
||||||
|
"""
|
||||||
|
raise IOError(errno.ESPIPE, "Illegal seek")
|
||||||
|
|
||||||
|
def tell(self):
|
||||||
|
"""Get the current file position."""
|
||||||
|
return self._transferred
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "blocking queue"
|
||||||
|
|
||||||
|
|
||||||
|
class ImageWriter(object):
|
||||||
|
"""Class to write the image to the image service from an input file."""
|
||||||
|
|
||||||
|
def __init__(self, context, input_file, image_service, image_id,
|
||||||
|
image_meta=None):
|
||||||
|
"""Initializes the image writer instance with given parameters.
|
||||||
|
|
||||||
|
:param context: write context needed by the image service
|
||||||
|
:param input_file: file to read the image data from
|
||||||
|
:param image_service: handle to image service
|
||||||
|
:param image_id: ID of the image in the image service
|
||||||
|
:param image_meta: image meta-data
|
||||||
|
"""
|
||||||
|
if not image_meta:
|
||||||
|
image_meta = {}
|
||||||
|
|
||||||
|
self._context = context
|
||||||
|
self._input_file = input_file
|
||||||
|
self._image_service = image_service
|
||||||
|
self._image_id = image_id
|
||||||
|
self._image_meta = image_meta
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the image write task.
|
||||||
|
|
||||||
|
:returns: the event indicating the status of the write task
|
||||||
|
"""
|
||||||
|
self._done = event.Event()
|
||||||
|
|
||||||
|
def _inner():
|
||||||
|
"""Task performing the image write operation.
|
||||||
|
|
||||||
|
This method performs image data transfer through an update call.
|
||||||
|
After the update, it waits until the image state becomes
|
||||||
|
'active', 'killed' or unknown. If the final state is not 'active'
|
||||||
|
an instance of ImageTransferException is thrown.
|
||||||
|
|
||||||
|
:raises: ImageTransferException
|
||||||
|
"""
|
||||||
|
LOG.debug("Calling image service update on image: %(image)s "
|
||||||
|
"with meta: %(meta)s",
|
||||||
|
{'image': self._image_id,
|
||||||
|
'meta': self._image_meta})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._image_service.update(self._context,
|
||||||
|
self._image_id,
|
||||||
|
self._image_meta,
|
||||||
|
data=self._input_file)
|
||||||
|
self._running = True
|
||||||
|
while self._running:
|
||||||
|
LOG.debug("Retrieving status of image: %s.",
|
||||||
|
self._image_id)
|
||||||
|
image_meta = self._image_service.show(self._context,
|
||||||
|
self._image_id)
|
||||||
|
image_status = image_meta.get('status')
|
||||||
|
if image_status == 'active':
|
||||||
|
self.stop()
|
||||||
|
LOG.debug("Image: %s is now active.",
|
||||||
|
self._image_id)
|
||||||
|
self._done.send(True)
|
||||||
|
elif image_status == 'killed':
|
||||||
|
self.stop()
|
||||||
|
excep_msg = (_("Image: %s is in killed state.") %
|
||||||
|
self._image_id)
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
excep = exceptions.ImageTransferException(excep_msg)
|
||||||
|
self._done.send_exception(excep)
|
||||||
|
elif image_status in ['saving', 'queued']:
|
||||||
|
LOG.debug("Image: %(image)s is in %(state)s state; "
|
||||||
|
"sleeping for %(sleep)d seconds.",
|
||||||
|
{'image': self._image_id,
|
||||||
|
'state': image_status,
|
||||||
|
'sleep': IMAGE_SERVICE_POLL_INTERVAL})
|
||||||
|
greenthread.sleep(IMAGE_SERVICE_POLL_INTERVAL)
|
||||||
|
else:
|
||||||
|
self.stop()
|
||||||
|
excep_msg = (_("Image: %(image)s is in unknown "
|
||||||
|
"state: %(state)s.") %
|
||||||
|
{'image': self._image_id,
|
||||||
|
'state': image_status})
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
excep = exceptions.ImageTransferException(excep_msg)
|
||||||
|
self._done.send_exception(excep)
|
||||||
|
except Exception as excep:
|
||||||
|
self.stop()
|
||||||
|
excep_msg = (_("Error occurred while writing image: %s") %
|
||||||
|
self._image_id)
|
||||||
|
LOG.exception(excep_msg)
|
||||||
|
excep = exceptions.ImageTransferException(excep_msg, excep)
|
||||||
|
self._done.send_exception(excep)
|
||||||
|
|
||||||
|
LOG.debug("Starting image write task for image: %(image)s with"
|
||||||
|
" source: %(source)s.",
|
||||||
|
{'source': self._input_file,
|
||||||
|
'image': self._image_id})
|
||||||
|
greenthread.spawn(_inner)
|
||||||
|
return self._done
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the image writing task."""
|
||||||
|
LOG.debug("Stopping the writing task for image: %s.",
|
||||||
|
self._image_id)
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait for the image writer task to complete.
|
||||||
|
|
||||||
|
This method returns True if the writer thread completes successfully.
|
||||||
|
In case of error, it raises ImageTransferException.
|
||||||
|
|
||||||
|
:raises ImageTransferException
|
||||||
|
"""
|
||||||
|
return self._done.wait()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""This is a NOP."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = "Image Writer <source = %s, dest = %s>" % (self._input_file,
|
||||||
|
self._image_id)
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
class FileReadWriteTask(object):
|
||||||
|
"""Task which reads data from the input file and writes to the output file.
|
||||||
|
|
||||||
|
This class defines the task which copies the given input file to the given
|
||||||
|
output file. The copy operation involves reading chunks of data from the
|
||||||
|
input file and writing the same to the output file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, input_file, output_file):
|
||||||
|
"""Initializes the read-write task with the given input parameters.
|
||||||
|
|
||||||
|
:param input_file: the input file handle
|
||||||
|
:param output_file: the output file handle
|
||||||
|
"""
|
||||||
|
self._input_file = input_file
|
||||||
|
self._output_file = output_file
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the file read - file write task.
|
||||||
|
|
||||||
|
:returns: the event indicating the status of the read-write task
|
||||||
|
"""
|
||||||
|
self._done = event.Event()
|
||||||
|
|
||||||
|
def _inner():
|
||||||
|
"""Task performing the file read-write operation."""
|
||||||
|
self._running = True
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
data = self._input_file.read(rw_handles.READ_CHUNKSIZE)
|
||||||
|
if not data:
|
||||||
|
LOG.debug("File read-write task is done.")
|
||||||
|
self.stop()
|
||||||
|
self._done.send(True)
|
||||||
|
self._output_file.write(data)
|
||||||
|
|
||||||
|
# update lease progress if applicable
|
||||||
|
if hasattr(self._input_file, "update_progress"):
|
||||||
|
self._input_file.update_progress()
|
||||||
|
if hasattr(self._output_file, "update_progress"):
|
||||||
|
self._output_file.update_progress()
|
||||||
|
|
||||||
|
greenthread.sleep(FILE_READ_WRITE_TASK_SLEEP_TIME)
|
||||||
|
except Exception as excep:
|
||||||
|
self.stop()
|
||||||
|
excep_msg = _("Error occurred during file read-write "
|
||||||
|
"task.")
|
||||||
|
LOG.exception(excep_msg)
|
||||||
|
excep = exceptions.ImageTransferException(excep_msg, excep)
|
||||||
|
self._done.send_exception(excep)
|
||||||
|
|
||||||
|
LOG.debug("Starting file read-write task with source: %(source)s "
|
||||||
|
"and destination: %(dest)s.",
|
||||||
|
{'source': self._input_file,
|
||||||
|
'dest': self._output_file})
|
||||||
|
greenthread.spawn(_inner)
|
||||||
|
return self._done
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the read-write task."""
|
||||||
|
LOG.debug("Stopping the file read-write task.")
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait for the file read-write task to complete.
|
||||||
|
|
||||||
|
This method returns True if the read-write thread completes
|
||||||
|
successfully. In case of error, it raises ImageTransferException.
|
||||||
|
|
||||||
|
:raises: ImageTransferException
|
||||||
|
"""
|
||||||
|
return self._done.wait()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = ("File Read-Write Task <source = %s, dest = %s>" %
|
||||||
|
(self._input_file, self._output_file))
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
# Functions to perform image transfer between VMware servers and image service.
|
||||||
|
|
||||||
|
|
||||||
|
def _start_transfer(context, timeout_secs, read_file_handle, max_data_size,
|
||||||
|
write_file_handle=None, image_service=None, image_id=None,
|
||||||
|
image_meta=None):
|
||||||
|
"""Start the image transfer.
|
||||||
|
|
||||||
|
The image reader reads the data from the image source and writes to the
|
||||||
|
blocking queue. The image source is always a file handle (VmdkReadHandle
|
||||||
|
or ImageReadHandle); therefore, a FileReadWriteTask is created for this
|
||||||
|
transfer. The image writer reads the data from the blocking queue and
|
||||||
|
writes it to the image destination. The image destination is either a
|
||||||
|
file or VMDK in VMware datastore or an image in the image service.
|
||||||
|
|
||||||
|
If the destination is a file or VMDK in VMware datastore, the method
|
||||||
|
creates a FileReadWriteTask which reads from the blocking queue and
|
||||||
|
writes to either FileWriteHandle or VmdkWriteHandle. In the case of
|
||||||
|
image service as the destination, an instance of ImageWriter task is
|
||||||
|
created which reads from the blocking queue and writes to the image
|
||||||
|
service.
|
||||||
|
|
||||||
|
:param context: write context needed for the image service
|
||||||
|
:param timeout_secs: time in seconds to wait for the transfer to complete
|
||||||
|
:param read_file_handle: handle to read data from
|
||||||
|
:param max_data_size: maximum transfer size
|
||||||
|
:param write_file_handle: handle to write data to; if this is None, then
|
||||||
|
param image_service and param image_id should
|
||||||
|
be set.
|
||||||
|
:param image_service: image service handle
|
||||||
|
:param image_id: ID of the image in the image service
|
||||||
|
:param image_meta: image meta-data
|
||||||
|
:raises: ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the blocking queue
|
||||||
|
blocking_queue = BlockingQueue(BLOCKING_QUEUE_SIZE, max_data_size)
|
||||||
|
|
||||||
|
# Create the image reader
|
||||||
|
reader = FileReadWriteTask(read_file_handle, blocking_queue)
|
||||||
|
|
||||||
|
# Create the image writer
|
||||||
|
if write_file_handle:
|
||||||
|
# File or VMDK in VMware datastore is the image destination
|
||||||
|
writer = FileReadWriteTask(blocking_queue, write_file_handle)
|
||||||
|
elif image_service and image_id:
|
||||||
|
# Image service image is the destination
|
||||||
|
writer = ImageWriter(context,
|
||||||
|
blocking_queue,
|
||||||
|
image_service,
|
||||||
|
image_id,
|
||||||
|
image_meta)
|
||||||
|
else:
|
||||||
|
excep_msg = _("No image destination given.")
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
raise ValueError(excep_msg)
|
||||||
|
|
||||||
|
# Start the reader and writer
|
||||||
|
LOG.debug("Starting image transfer with reader: %(reader)s and writer: "
|
||||||
|
"%(writer)s",
|
||||||
|
{'reader': reader,
|
||||||
|
'writer': writer})
|
||||||
|
reader.start()
|
||||||
|
writer.start()
|
||||||
|
timer = timeout.Timeout(timeout_secs)
|
||||||
|
try:
|
||||||
|
# Wait for the reader and writer to complete
|
||||||
|
reader.wait()
|
||||||
|
writer.wait()
|
||||||
|
except (timeout.Timeout, exceptions.ImageTransferException) as excep:
|
||||||
|
excep_msg = (_("Error occurred during image transfer with reader: "
|
||||||
|
"%(reader)s and writer: %(writer)s") %
|
||||||
|
{'reader': reader,
|
||||||
|
'writer': writer})
|
||||||
|
LOG.exception(excep_msg)
|
||||||
|
reader.stop()
|
||||||
|
writer.stop()
|
||||||
|
|
||||||
|
if isinstance(excep, exceptions.ImageTransferException):
|
||||||
|
raise
|
||||||
|
raise exceptions.ImageTransferException(excep_msg, excep)
|
||||||
|
finally:
|
||||||
|
timer.cancel()
|
||||||
|
read_file_handle.close()
|
||||||
|
if write_file_handle:
|
||||||
|
write_file_handle.close()
|
||||||
|
|
||||||
|
|
||||||
|
def download_image(image, image_meta, session, datastore, rel_path,
|
||||||
|
bypass=True, timeout_secs=7200):
|
||||||
|
"""Transfer an image to a datastore.
|
||||||
|
|
||||||
|
:param image: file-like iterator
|
||||||
|
:param image_meta: image metadata
|
||||||
|
:param session: VMwareAPISession object
|
||||||
|
:param datastore: Datastore object
|
||||||
|
:param rel_path: path where the file will be stored in the datastore
|
||||||
|
:param bypass: if set to True, bypass vCenter to download the image
|
||||||
|
:param timeout_secs: time in seconds to wait for the xfer to complete
|
||||||
|
"""
|
||||||
|
image_size = int(image_meta['size'])
|
||||||
|
method = 'PUT'
|
||||||
|
if bypass:
|
||||||
|
hosts = datastore.get_connected_hosts(session)
|
||||||
|
host = ds_obj.Datastore.choose_host(hosts)
|
||||||
|
host_name = session.invoke_api(vim_util, 'get_object_property',
|
||||||
|
session.vim, host, 'name')
|
||||||
|
ds_url = datastore.build_url(session._scheme, host_name, rel_path,
|
||||||
|
constants.ESX_DATACENTER_PATH)
|
||||||
|
cookie = ds_url.get_transfer_ticket(session, method)
|
||||||
|
conn = ds_url.connect(method, image_size, cookie)
|
||||||
|
else:
|
||||||
|
ds_url = datastore.build_url(session._scheme, session._host, rel_path)
|
||||||
|
cookie = '%s=%s' % (constants.SOAP_COOKIE_KEY,
|
||||||
|
session.vim.get_http_cookie().strip("\""))
|
||||||
|
conn = ds_url.connect(method, image_size, cookie)
|
||||||
|
conn.write = conn.send
|
||||||
|
|
||||||
|
read_handle = rw_handles.ImageReadHandle(image)
|
||||||
|
_start_transfer(None, timeout_secs, read_handle, image_size,
|
||||||
|
write_file_handle=conn)
|
||||||
|
|
||||||
|
|
||||||
|
def download_flat_image(context, timeout_secs, image_service, image_id,
|
||||||
|
**kwargs):
|
||||||
|
"""Download flat image from the image service to VMware server.
|
||||||
|
|
||||||
|
:param context: image service write context
|
||||||
|
:param timeout_secs: time in seconds to wait for the download to complete
|
||||||
|
:param image_service: image service handle
|
||||||
|
:param image_id: ID of the image to be downloaded
|
||||||
|
:param kwargs: keyword arguments to configure the destination
|
||||||
|
file write handle
|
||||||
|
:raises: VimConnectionException, ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
LOG.debug("Downloading image: %s from image service as a flat file.",
|
||||||
|
image_id)
|
||||||
|
|
||||||
|
# TODO(vbala) catch specific exceptions raised by download call
|
||||||
|
read_iter = image_service.download(context, image_id)
|
||||||
|
read_handle = rw_handles.ImageReadHandle(read_iter)
|
||||||
|
file_size = int(kwargs.get('image_size'))
|
||||||
|
write_handle = rw_handles.FileWriteHandle(kwargs.get('host'),
|
||||||
|
kwargs.get('port'),
|
||||||
|
kwargs.get('data_center_name'),
|
||||||
|
kwargs.get('datastore_name'),
|
||||||
|
kwargs.get('cookies'),
|
||||||
|
kwargs.get('file_path'),
|
||||||
|
file_size,
|
||||||
|
cacerts=kwargs.get('cacerts'))
|
||||||
|
_start_transfer(context,
|
||||||
|
timeout_secs,
|
||||||
|
read_handle,
|
||||||
|
file_size,
|
||||||
|
write_file_handle=write_handle)
|
||||||
|
LOG.debug("Downloaded image: %s from image service as a flat file.",
|
||||||
|
image_id)
|
||||||
|
|
||||||
|
|
||||||
|
def download_stream_optimized_data(context, timeout_secs, read_handle,
|
||||||
|
**kwargs):
|
||||||
|
"""Download stream optimized data to VMware server.
|
||||||
|
|
||||||
|
:param context: image service write context
|
||||||
|
:param timeout_secs: time in seconds to wait for the download to complete
|
||||||
|
:param read_handle: handle from which to read the image data
|
||||||
|
:param kwargs: keyword arguments to configure the destination
|
||||||
|
VMDK write handle
|
||||||
|
:returns: managed object reference of the VM created for import to VMware
|
||||||
|
server
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException,
|
||||||
|
ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
file_size = int(kwargs.get('image_size'))
|
||||||
|
write_handle = rw_handles.VmdkWriteHandle(kwargs.get('session'),
|
||||||
|
kwargs.get('host'),
|
||||||
|
kwargs.get('port'),
|
||||||
|
kwargs.get('resource_pool'),
|
||||||
|
kwargs.get('vm_folder'),
|
||||||
|
kwargs.get('vm_import_spec'),
|
||||||
|
file_size)
|
||||||
|
_start_transfer(context,
|
||||||
|
timeout_secs,
|
||||||
|
read_handle,
|
||||||
|
file_size,
|
||||||
|
write_file_handle=write_handle)
|
||||||
|
return write_handle.get_imported_vm()
|
||||||
|
|
||||||
|
|
||||||
|
def download_stream_optimized_image(context, timeout_secs, image_service,
|
||||||
|
image_id, **kwargs):
|
||||||
|
"""Download stream optimized image from image service to VMware server.
|
||||||
|
|
||||||
|
:param context: image service write context
|
||||||
|
:param timeout_secs: time in seconds to wait for the download to complete
|
||||||
|
:param image_service: image service handle
|
||||||
|
:param image_id: ID of the image to be downloaded
|
||||||
|
:param kwargs: keyword arguments to configure the destination
|
||||||
|
VMDK write handle
|
||||||
|
:returns: managed object reference of the VM created for import to VMware
|
||||||
|
server
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException,
|
||||||
|
ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
LOG.debug("Downloading image: %s from image service as a stream "
|
||||||
|
"optimized file.",
|
||||||
|
image_id)
|
||||||
|
|
||||||
|
# TODO(vbala) catch specific exceptions raised by download call
|
||||||
|
read_iter = image_service.download(context, image_id)
|
||||||
|
read_handle = rw_handles.ImageReadHandle(read_iter)
|
||||||
|
imported_vm = download_stream_optimized_data(context, timeout_secs,
|
||||||
|
read_handle, **kwargs)
|
||||||
|
|
||||||
|
LOG.debug("Downloaded image: %s from image service as a stream "
|
||||||
|
"optimized file.",
|
||||||
|
image_id)
|
||||||
|
return imported_vm
|
||||||
|
|
||||||
|
|
||||||
|
def copy_stream_optimized_disk(
|
||||||
|
context, timeout_secs, write_handle, **kwargs):
|
||||||
|
"""Copy virtual disk from VMware server to the given write handle.
|
||||||
|
|
||||||
|
:param context: context
|
||||||
|
:param timeout_secs: time in seconds to wait for the copy to complete
|
||||||
|
:param write_handle: copy destination
|
||||||
|
:param kwargs: keyword arguments to configure the source
|
||||||
|
VMDK read handle
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException,
|
||||||
|
ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
vmdk_file_path = kwargs.get('vmdk_file_path')
|
||||||
|
LOG.debug("Copying virtual disk: %(vmdk_path)s to %(dest)s.",
|
||||||
|
{'vmdk_path': vmdk_file_path,
|
||||||
|
'dest': write_handle.name})
|
||||||
|
file_size = kwargs.get('vmdk_size')
|
||||||
|
read_handle = rw_handles.VmdkReadHandle(kwargs.get('session'),
|
||||||
|
kwargs.get('host'),
|
||||||
|
kwargs.get('port'),
|
||||||
|
kwargs.get('vm'),
|
||||||
|
kwargs.get('vmdk_file_path'),
|
||||||
|
file_size)
|
||||||
|
_start_transfer(context, timeout_secs, read_handle, file_size,
|
||||||
|
write_file_handle=write_handle)
|
||||||
|
LOG.debug("Downloaded virtual disk: %s.", vmdk_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_image(context, timeout_secs, image_service, image_id, owner_id,
|
||||||
|
**kwargs):
|
||||||
|
"""Upload the VM's disk file to image service.
|
||||||
|
|
||||||
|
:param context: image service write context
|
||||||
|
:param timeout_secs: time in seconds to wait for the upload to complete
|
||||||
|
:param image_service: image service handle
|
||||||
|
:param image_id: upload destination image ID
|
||||||
|
:param kwargs: keyword arguments to configure the source
|
||||||
|
VMDK read handle
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException,
|
||||||
|
ImageTransferException, ValueError
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug("Uploading to image: %s.", image_id)
|
||||||
|
file_size = kwargs.get('vmdk_size')
|
||||||
|
read_handle = rw_handles.VmdkReadHandle(kwargs.get('session'),
|
||||||
|
kwargs.get('host'),
|
||||||
|
kwargs.get('port'),
|
||||||
|
kwargs.get('vm'),
|
||||||
|
kwargs.get('vmdk_file_path'),
|
||||||
|
file_size)
|
||||||
|
|
||||||
|
# Set the image properties. It is important to set the 'size' to 0.
|
||||||
|
# Otherwise, the image service client will use the VM's disk capacity
|
||||||
|
# which will not be the image size after upload, since it is converted
|
||||||
|
# to a stream-optimized sparse disk.
|
||||||
|
image_metadata = {'disk_format': 'vmdk',
|
||||||
|
'is_public': kwargs.get('is_public'),
|
||||||
|
'name': kwargs.get('image_name'),
|
||||||
|
'status': 'active',
|
||||||
|
'container_format': 'bare',
|
||||||
|
'size': 0,
|
||||||
|
'properties': {'vmware_image_version':
|
||||||
|
kwargs.get('image_version'),
|
||||||
|
'vmware_disktype': 'streamOptimized',
|
||||||
|
'owner_id': owner_id}}
|
||||||
|
|
||||||
|
# Passing 0 as the file size since data size to be transferred cannot be
|
||||||
|
# predetermined.
|
||||||
|
_start_transfer(context,
|
||||||
|
timeout_secs,
|
||||||
|
read_handle,
|
||||||
|
0,
|
||||||
|
image_service=image_service,
|
||||||
|
image_id=image_id,
|
||||||
|
image_meta=image_metadata)
|
||||||
|
LOG.debug("Uploaded image: %s.", image_id)
|
0
oslo_vmware/objects/__init__.py
Normal file
0
oslo_vmware/objects/__init__.py
Normal file
27
oslo_vmware/objects/datacenter.py
Normal file
27
oslo_vmware/objects/datacenter.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Copyright (c) 2014 VMware, Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class Datacenter(object):
|
||||||
|
|
||||||
|
def __init__(self, ref, name):
|
||||||
|
"""Datacenter object holds ref and name together for convenience."""
|
||||||
|
if name is None:
|
||||||
|
raise ValueError(_("Datacenter name cannot be None"))
|
||||||
|
if ref is None:
|
||||||
|
raise ValueError(_("Datacenter reference cannot be None"))
|
||||||
|
self.ref = ref
|
||||||
|
self.name = name
|
318
oslo_vmware/objects/datastore.py
Normal file
318
oslo_vmware/objects/datastore.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
# Copyright (c) 2014 VMware, Inc.
|
||||||
|
#
|
||||||
|
# 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 logging
|
||||||
|
import posixpath
|
||||||
|
import random
|
||||||
|
|
||||||
|
import six.moves.http_client as httplib
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
|
from oslo_vmware import constants
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Datastore(object):
|
||||||
|
|
||||||
|
def __init__(self, ref, name, capacity=None, freespace=None,
|
||||||
|
type=None, datacenter=None):
|
||||||
|
"""Datastore object holds ref and name together for convenience.
|
||||||
|
|
||||||
|
:param ref: a vSphere reference to a datastore
|
||||||
|
:param name: vSphere unique name for this datastore
|
||||||
|
:param capacity: (optional) capacity in bytes of this datastore
|
||||||
|
:param freespace: (optional) free space in bytes of datastore
|
||||||
|
:param type: (optional) datastore type
|
||||||
|
:param datacenter: (optional) oslo_vmware Datacenter object
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
raise ValueError(_("Datastore name cannot be None"))
|
||||||
|
if ref is None:
|
||||||
|
raise ValueError(_("Datastore reference cannot be None"))
|
||||||
|
if freespace is not None and capacity is None:
|
||||||
|
raise ValueError(_("Invalid capacity"))
|
||||||
|
if capacity is not None and freespace is not None:
|
||||||
|
if capacity < freespace:
|
||||||
|
raise ValueError(_("Capacity is smaller than free space"))
|
||||||
|
|
||||||
|
self.ref = ref
|
||||||
|
self.name = name
|
||||||
|
self.capacity = capacity
|
||||||
|
self.freespace = freespace
|
||||||
|
self.type = type
|
||||||
|
self.datacenter = datacenter
|
||||||
|
|
||||||
|
def build_path(self, *paths):
|
||||||
|
"""Constructs and returns a DatastorePath.
|
||||||
|
|
||||||
|
:param paths: list of path components, for constructing a path relative
|
||||||
|
to the root directory of the datastore
|
||||||
|
:return: a DatastorePath object
|
||||||
|
"""
|
||||||
|
return DatastorePath(self.name, *paths)
|
||||||
|
|
||||||
|
def build_url(self, scheme, server, rel_path, datacenter_name=None):
|
||||||
|
"""Constructs and returns a DatastoreURL.
|
||||||
|
|
||||||
|
:param scheme: scheme of the URL (http, https).
|
||||||
|
:param server: hostname or ip
|
||||||
|
:param rel_path: relative path of the file on the datastore
|
||||||
|
:param datacenter_name: (optional) datacenter name
|
||||||
|
:return: a DatastoreURL object
|
||||||
|
"""
|
||||||
|
if self.datacenter is None and datacenter_name is None:
|
||||||
|
raise ValueError(_("datacenter must be set to build url"))
|
||||||
|
if datacenter_name is None:
|
||||||
|
datacenter_name = self.datacenter.name
|
||||||
|
return DatastoreURL(scheme, server, rel_path, datacenter_name,
|
||||||
|
self.name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '[%s]' % self._name
|
||||||
|
|
||||||
|
def get_summary(self, session):
|
||||||
|
"""Get datastore summary.
|
||||||
|
|
||||||
|
:param datastore: Reference to the datastore
|
||||||
|
:return: 'summary' property of the datastore
|
||||||
|
"""
|
||||||
|
return session.invoke_api(vim_util, 'get_object_property',
|
||||||
|
session.vim, self.ref, 'summary')
|
||||||
|
|
||||||
|
def get_connected_hosts(self, session):
|
||||||
|
"""Get a list of usable (accessible, mounted, read-writable) hosts where
|
||||||
|
the datastore is mounted.
|
||||||
|
|
||||||
|
:param: session: session
|
||||||
|
:return: list of HostSystem managed object references
|
||||||
|
"""
|
||||||
|
hosts = []
|
||||||
|
summary = self.get_summary(session)
|
||||||
|
if not summary.accessible:
|
||||||
|
return hosts
|
||||||
|
host_mounts = session.invoke_api(vim_util, 'get_object_property',
|
||||||
|
session.vim, self.ref, 'host')
|
||||||
|
if not hasattr(host_mounts, 'DatastoreHostMount'):
|
||||||
|
return hosts
|
||||||
|
for host_mount in host_mounts.DatastoreHostMount:
|
||||||
|
if self.is_datastore_mount_usable(host_mount.mountInfo):
|
||||||
|
hosts.append(host_mount.key)
|
||||||
|
return hosts
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_datastore_mount_usable(mount_info):
|
||||||
|
"""Check if a datastore is usable as per the given mount info.
|
||||||
|
|
||||||
|
The datastore is considered to be usable for a host only if it is
|
||||||
|
writable, mounted and accessible.
|
||||||
|
|
||||||
|
:param mount_info: HostMountInfo data object
|
||||||
|
:return: True if datastore is usable
|
||||||
|
"""
|
||||||
|
writable = mount_info.accessMode == 'readWrite'
|
||||||
|
mounted = getattr(mount_info, 'mounted', True)
|
||||||
|
accessible = getattr(mount_info, 'accessible', False)
|
||||||
|
|
||||||
|
return writable and mounted and accessible
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def choose_host(hosts):
|
||||||
|
i = random.randrange(0, len(hosts))
|
||||||
|
return hosts[i]
|
||||||
|
|
||||||
|
|
||||||
|
class DatastorePath(object):
|
||||||
|
|
||||||
|
"""Class for representing a directory or file path in a vSphere datatore.
|
||||||
|
|
||||||
|
This provides various helper methods to access components and useful
|
||||||
|
variants of the datastore path.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
DatastorePath("datastore1", "_base/foo", "foo.vmdk") creates an
|
||||||
|
object that describes the "[datastore1] _base/foo/foo.vmdk" datastore
|
||||||
|
file path to a virtual disk.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Datastore path representations always uses forward slash as separator
|
||||||
|
(hence the use of the posixpath module).
|
||||||
|
- Datastore names are enclosed in square brackets.
|
||||||
|
- Path part of datastore path is relative to the root directory
|
||||||
|
of the datastore, and is always separated from the [ds_name] part with
|
||||||
|
a single space.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, datastore_name, *paths):
|
||||||
|
if datastore_name is None or datastore_name == '':
|
||||||
|
raise ValueError(_("Datastore name cannot be empty"))
|
||||||
|
self._datastore_name = datastore_name
|
||||||
|
self._rel_path = ''
|
||||||
|
if paths:
|
||||||
|
if None in paths:
|
||||||
|
raise ValueError(_("Path component cannot be None"))
|
||||||
|
self._rel_path = posixpath.join(*paths)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Full datastore path to the file or directory."""
|
||||||
|
if self._rel_path != '':
|
||||||
|
return "[%s] %s" % (self._datastore_name, self.rel_path)
|
||||||
|
return "[%s]" % self._datastore_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datastore(self):
|
||||||
|
return self._datastore_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return DatastorePath(self.datastore, posixpath.dirname(self._rel_path))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def basename(self):
|
||||||
|
return posixpath.basename(self._rel_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dirname(self):
|
||||||
|
return posixpath.dirname(self._rel_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rel_path(self):
|
||||||
|
return self._rel_path
|
||||||
|
|
||||||
|
def join(self, *paths):
|
||||||
|
"""Join one or more path components intelligently into a datastore path.
|
||||||
|
|
||||||
|
If any component is an absolute path, all previous components are
|
||||||
|
thrown away, and joining continues. The return value is the
|
||||||
|
concatenation of the paths with exactly one slash ('/') inserted
|
||||||
|
between components, unless p is empty.
|
||||||
|
|
||||||
|
:return: A datastore path
|
||||||
|
"""
|
||||||
|
if paths:
|
||||||
|
if None in paths:
|
||||||
|
raise ValueError(_("Path component cannot be None"))
|
||||||
|
return DatastorePath(self.datastore, self._rel_path, *paths)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, DatastorePath) and
|
||||||
|
self._datastore_name == other._datastore_name and
|
||||||
|
self._rel_path == other._rel_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, datastore_path):
|
||||||
|
"""Constructs a DatastorePath object given a datastore path string."""
|
||||||
|
if not datastore_path:
|
||||||
|
raise ValueError(_("Datastore path cannot be empty"))
|
||||||
|
|
||||||
|
spl = datastore_path.split('[', 1)[1].split(']', 1)
|
||||||
|
path = ""
|
||||||
|
if len(spl) == 1:
|
||||||
|
datastore_name = spl[0]
|
||||||
|
else:
|
||||||
|
datastore_name, path = spl
|
||||||
|
return cls(datastore_name, path.strip())
|
||||||
|
|
||||||
|
|
||||||
|
class DatastoreURL(object):
|
||||||
|
|
||||||
|
"""Class for representing a URL to HTTP access a file in a datastore.
|
||||||
|
|
||||||
|
This provides various helper methods to access components and useful
|
||||||
|
variants of the datastore URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, scheme, server, path, datacenter_path, datastore_name):
|
||||||
|
self._scheme = scheme
|
||||||
|
self._server = server
|
||||||
|
self._path = path
|
||||||
|
self._datacenter_path = datacenter_path
|
||||||
|
self._datastore_name = datastore_name
|
||||||
|
params = {'dcPath': self._datacenter_path,
|
||||||
|
'dsName': self._datastore_name}
|
||||||
|
self._query = urlparse.urlencode(params)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def urlparse(cls, url):
|
||||||
|
scheme, server, path, params, query, fragment = urlparse.urlparse(url)
|
||||||
|
if not query:
|
||||||
|
path = path.split('?')
|
||||||
|
query = path[1]
|
||||||
|
path = path[0]
|
||||||
|
params = urlparse.parse_qs(query)
|
||||||
|
dc_path = params.get('dcPath')
|
||||||
|
if dc_path is not None and len(dc_path) > 0:
|
||||||
|
datacenter_path = dc_path[0]
|
||||||
|
ds_name = params.get('dsName')
|
||||||
|
if ds_name is not None and len(ds_name) > 0:
|
||||||
|
datastore_name = ds_name[0]
|
||||||
|
path = path[len('/folder'):]
|
||||||
|
return cls(scheme, server, path, datacenter_path, datastore_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self._path.strip('/')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datacenter_path(self):
|
||||||
|
return self._datacenter_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datastore_name(self):
|
||||||
|
return self._datastore_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s://%s/folder/%s?%s' % (self._scheme, self._server,
|
||||||
|
self.path, self._query)
|
||||||
|
|
||||||
|
def connect(self, method, content_length, cookie):
|
||||||
|
try:
|
||||||
|
if self._scheme == 'http':
|
||||||
|
conn = httplib.HTTPConnection(self._server)
|
||||||
|
elif self._scheme == 'https':
|
||||||
|
conn = httplib.HTTPSConnection(self._server)
|
||||||
|
else:
|
||||||
|
excep_msg = _("Invalid scheme: %s.") % self._scheme
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
raise ValueError(excep_msg)
|
||||||
|
conn.putrequest(method, '/folder/%s?%s' % (self.path, self._query))
|
||||||
|
conn.putheader('User-Agent', constants.USER_AGENT)
|
||||||
|
conn.putheader('Content-Length', content_length)
|
||||||
|
conn.putheader('Cookie', cookie)
|
||||||
|
conn.endheaders()
|
||||||
|
LOG.debug("Created HTTP connection to transfer the file with "
|
||||||
|
"URL = %s.", str(self))
|
||||||
|
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.") % str(self)
|
||||||
|
LOG.exception(excep_msg)
|
||||||
|
raise exceptions.VimConnectionException(excep_msg, excep)
|
||||||
|
|
||||||
|
def get_transfer_ticket(self, session, method):
|
||||||
|
client_factory = session.vim.client.factory
|
||||||
|
spec = vim_util.get_http_service_request_spec(client_factory, method,
|
||||||
|
str(self))
|
||||||
|
ticket = session.invoke_api(
|
||||||
|
session.vim,
|
||||||
|
'AcquireGenericServiceTicket',
|
||||||
|
session.vim.service_content.sessionManager,
|
||||||
|
spec=spec)
|
||||||
|
return '%s="%s"' % (constants.CGI_COOKIE_KEY, ticket.id)
|
200
oslo_vmware/pbm.py
Normal file
200
oslo_vmware/pbm.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
VMware PBM service client and PBM related utility methods
|
||||||
|
|
||||||
|
PBM is used for policy based placement in VMware datastores.
|
||||||
|
Refer http://goo.gl/GR2o6U for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
import six.moves.urllib.request as urllib
|
||||||
|
import suds.sax.element as element
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _LW
|
||||||
|
from oslo_vmware import service
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
SERVICE_TYPE = 'PbmServiceInstance'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Pbm(service.Service):
|
||||||
|
"""Service class that provides access to the Storage Policy API."""
|
||||||
|
|
||||||
|
def __init__(self, protocol='https', host='localhost', port=443,
|
||||||
|
wsdl_url=None, cacert=None, insecure=True):
|
||||||
|
"""Constructs a PBM service client object.
|
||||||
|
|
||||||
|
:param protocol: http or https
|
||||||
|
:param host: server IP address or host name
|
||||||
|
:param port: port for connection
|
||||||
|
:param wsdl_url: PBM WSDL url
|
||||||
|
:param cacert: Specify a CA bundle file to use in verifying a
|
||||||
|
TLS (https) server certificate.
|
||||||
|
:param insecure: Verify HTTPS connections using system certificates,
|
||||||
|
used only if cacert is not specified
|
||||||
|
"""
|
||||||
|
base_url = service.Service.build_base_url(protocol, host, port)
|
||||||
|
soap_url = base_url + '/pbm'
|
||||||
|
super(Pbm, self).__init__(wsdl_url, soap_url, cacert, insecure)
|
||||||
|
|
||||||
|
def set_soap_cookie(self, cookie):
|
||||||
|
"""Set the specified vCenter session cookie in the SOAP header
|
||||||
|
|
||||||
|
:param cookie: cookie to set
|
||||||
|
"""
|
||||||
|
elem = element.Element('vcSessionCookie').setText(cookie)
|
||||||
|
self.client.set_options(soapheaders=elem)
|
||||||
|
|
||||||
|
def retrieve_service_content(self):
|
||||||
|
ref = vim_util.get_moref(service.SERVICE_INSTANCE, SERVICE_TYPE)
|
||||||
|
return self.PbmRetrieveServiceContent(ref)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "PBM Object"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "PBM Object"
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_profiles(session):
|
||||||
|
"""Get all the profiles defined in VC server.
|
||||||
|
|
||||||
|
:returns: PbmProfile data objects
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
LOG.debug("Fetching all the profiles defined in VC server.")
|
||||||
|
|
||||||
|
pbm = session.pbm
|
||||||
|
profile_manager = pbm.service_content.profileManager
|
||||||
|
res_type = pbm.client.factory.create('ns0:PbmProfileResourceType')
|
||||||
|
res_type.resourceType = 'STORAGE'
|
||||||
|
profiles = []
|
||||||
|
profile_ids = session.invoke_api(pbm,
|
||||||
|
'PbmQueryProfile',
|
||||||
|
profile_manager,
|
||||||
|
resourceType=res_type)
|
||||||
|
LOG.debug("Fetched profile IDs: %s.", profile_ids)
|
||||||
|
if profile_ids:
|
||||||
|
profiles = session.invoke_api(pbm,
|
||||||
|
'PbmRetrieveContent',
|
||||||
|
profile_manager,
|
||||||
|
profileIds=profile_ids)
|
||||||
|
return profiles
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_id_by_name(session, profile_name):
|
||||||
|
"""Get the profile UUID corresponding to the given profile name.
|
||||||
|
|
||||||
|
:param profile_name: profile name whose UUID needs to be retrieved
|
||||||
|
:returns: profile UUID string or None if profile not found
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
LOG.debug("Retrieving profile ID for profile: %s.", profile_name)
|
||||||
|
for profile in get_all_profiles(session):
|
||||||
|
if profile.name == profile_name:
|
||||||
|
profile_id = profile.profileId
|
||||||
|
LOG.debug("Retrieved profile ID: %(id)s for profile: %(name)s.",
|
||||||
|
{'id': profile_id,
|
||||||
|
'name': profile_name})
|
||||||
|
return profile_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def filter_hubs_by_profile(session, hubs, profile_id):
|
||||||
|
"""Filter and return hubs that match the given profile.
|
||||||
|
|
||||||
|
:param hubs: PbmPlacementHub morefs
|
||||||
|
:param profile_id: profile ID
|
||||||
|
:returns: subset of hubs that match the given profile
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
LOG.debug("Filtering hubs: %(hubs)s that match profile: %(profile)s.",
|
||||||
|
{'hubs': hubs,
|
||||||
|
'profile': profile_id})
|
||||||
|
|
||||||
|
pbm = session.pbm
|
||||||
|
placement_solver = pbm.service_content.placementSolver
|
||||||
|
filtered_hubs = session.invoke_api(pbm,
|
||||||
|
'PbmQueryMatchingHub',
|
||||||
|
placement_solver,
|
||||||
|
hubsToSearch=hubs,
|
||||||
|
profile=profile_id)
|
||||||
|
LOG.debug("Filtered hubs: %s", filtered_hubs)
|
||||||
|
return filtered_hubs
|
||||||
|
|
||||||
|
|
||||||
|
def convert_datastores_to_hubs(pbm_client_factory, datastores):
|
||||||
|
"""Convert given datastore morefs to PbmPlacementHub morefs.
|
||||||
|
|
||||||
|
:param pbm_client_factory: Factory to create PBM API input specs
|
||||||
|
:param datastores: list of datastore morefs
|
||||||
|
:returns: list of PbmPlacementHub morefs
|
||||||
|
"""
|
||||||
|
hubs = []
|
||||||
|
for ds in datastores:
|
||||||
|
hub = pbm_client_factory.create('ns0:PbmPlacementHub')
|
||||||
|
hub.hubId = ds.value
|
||||||
|
hub.hubType = 'Datastore'
|
||||||
|
hubs.append(hub)
|
||||||
|
return hubs
|
||||||
|
|
||||||
|
|
||||||
|
def filter_datastores_by_hubs(hubs, datastores):
|
||||||
|
"""Get filtered subset of datastores corresponding to the given hub list.
|
||||||
|
|
||||||
|
:param hubs: list of PbmPlacementHub morefs
|
||||||
|
:param datastores: all candidate datastores
|
||||||
|
:returns: subset of datastores corresponding to the given hub list
|
||||||
|
"""
|
||||||
|
filtered_dss = []
|
||||||
|
hub_ids = [hub.hubId for hub in hubs]
|
||||||
|
for ds in datastores:
|
||||||
|
if ds.value in hub_ids:
|
||||||
|
filtered_dss.append(ds)
|
||||||
|
return filtered_dss
|
||||||
|
|
||||||
|
|
||||||
|
def get_pbm_wsdl_location(vc_version):
|
||||||
|
"""Return PBM WSDL file location corresponding to VC version.
|
||||||
|
|
||||||
|
:param vc_version: a dot-separated version string. For example, "1.2".
|
||||||
|
:return: the pbm wsdl file location.
|
||||||
|
"""
|
||||||
|
if not vc_version:
|
||||||
|
return
|
||||||
|
ver = vc_version.split('.')
|
||||||
|
major_minor = ver[0]
|
||||||
|
if len(ver) >= 2:
|
||||||
|
major_minor = '%s.%s' % (major_minor, ver[1])
|
||||||
|
curr_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
pbm_service_wsdl = os.path.join(curr_dir, 'wsdl', major_minor,
|
||||||
|
'pbmService.wsdl')
|
||||||
|
if not os.path.exists(pbm_service_wsdl):
|
||||||
|
LOG.warn(_LW("PBM WSDL file %s not found."), pbm_service_wsdl)
|
||||||
|
return
|
||||||
|
pbm_wsdl = urlparse.urljoin('file:', urllib.pathname2url(pbm_service_wsdl))
|
||||||
|
LOG.debug("Using PBM WSDL location: %s.", pbm_wsdl)
|
||||||
|
return pbm_wsdl
|
632
oslo_vmware/rw_handles.py
Normal file
632
oslo_vmware/rw_handles.py
Normal file
@ -0,0 +1,632 @@
|
|||||||
|
# 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 logging
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
from urllib3 import connection as httplib
|
||||||
|
|
||||||
|
from oslo.utils import excutils
|
||||||
|
from oslo.utils import netutils
|
||||||
|
from oslo_vmware._i18n import _, _LE, _LW
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MIN_PROGRESS_DIFF_TO_LOG = 25
|
||||||
|
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
|
||||||
|
self._last_logged_progress = 0
|
||||||
|
|
||||||
|
def _create_read_connection(self, url, cookies=None, cacerts=False):
|
||||||
|
LOG.debug("Opening URL: %s for reading.", url)
|
||||||
|
try:
|
||||||
|
headers = {'User-Agent': USER_AGENT}
|
||||||
|
if cookies:
|
||||||
|
headers.update({'Cookie':
|
||||||
|
self._build_vim_cookie_header(cookies)})
|
||||||
|
response = requests.get(url, headers=headers, stream=True,
|
||||||
|
verify=cacerts)
|
||||||
|
return response.raw
|
||||||
|
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 _create_write_connection(self, url,
|
||||||
|
file_size=None,
|
||||||
|
cookies=None,
|
||||||
|
overwrite=None,
|
||||||
|
content_type=None,
|
||||||
|
cacerts=False):
|
||||||
|
"""Create HTTP connection to write to VMDK file."""
|
||||||
|
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)
|
||||||
|
cert_reqs = None
|
||||||
|
|
||||||
|
# cacerts can be either True or False or contain
|
||||||
|
# actual certificates. If it is a boolean, then
|
||||||
|
# we need to set cert_reqs and clear the cacerts
|
||||||
|
if isinstance(cacerts, bool):
|
||||||
|
if cacerts:
|
||||||
|
cert_reqs = ssl.CERT_REQUIRED
|
||||||
|
else:
|
||||||
|
cert_reqs = ssl.CERT_NONE
|
||||||
|
cacerts = None
|
||||||
|
|
||||||
|
conn.set_cert(ca_certs=cacerts, cert_reqs=cert_reqs)
|
||||||
|
else:
|
||||||
|
excep_msg = _("Invalid scheme: %s.") % scheme
|
||||||
|
LOG.error(excep_msg)
|
||||||
|
raise ValueError(excep_msg)
|
||||||
|
|
||||||
|
if query:
|
||||||
|
path = path + '?' + query
|
||||||
|
|
||||||
|
headers = {'User-Agent': USER_AGENT}
|
||||||
|
if file_size:
|
||||||
|
headers.update({'Content-Length': str(file_size)})
|
||||||
|
if overwrite:
|
||||||
|
headers.update({'Overwrite': overwrite})
|
||||||
|
if cookies:
|
||||||
|
headers.update({'Cookie':
|
||||||
|
self._build_vim_cookie_header(cookies)})
|
||||||
|
if content_type:
|
||||||
|
headers.update({'Content-Type': content_type})
|
||||||
|
|
||||||
|
conn.putrequest('PUT', path)
|
||||||
|
for key, value in six.iteritems(headers):
|
||||||
|
conn.putheader(key, value)
|
||||||
|
conn.endheaders()
|
||||||
|
return conn
|
||||||
|
except requests.RequestException 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 close(self):
|
||||||
|
"""Close the file handle."""
|
||||||
|
try:
|
||||||
|
self._file_handle.close()
|
||||||
|
except Exception:
|
||||||
|
LOG.warn(_LW("Error occurred while closing the file handle"),
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
|
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 _get_soap_url(self, scheme, host, port):
|
||||||
|
"""Returns the IPv4/v6 compatible SOAP URL for the given host."""
|
||||||
|
if netutils.is_valid_ipv6(host):
|
||||||
|
return '%s://[%s]:%d' % (scheme, host, port)
|
||||||
|
return '%s://%s:%d' % (scheme, host, port)
|
||||||
|
|
||||||
|
def _fix_esx_url(self, url, host, port):
|
||||||
|
"""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
|
||||||
|
if netutils.is_valid_ipv6(host):
|
||||||
|
netloc = '[%s]:%d' % (host, port)
|
||||||
|
else:
|
||||||
|
netloc = "%s:%d" % (host, port)
|
||||||
|
url = urlparse.urlunparse((scheme,
|
||||||
|
netloc,
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
fragment))
|
||||||
|
return url
|
||||||
|
|
||||||
|
def _find_vmdk_url(self, lease_info, host, port):
|
||||||
|
"""Find the URL corresponding to a VMDK file in lease info."""
|
||||||
|
url = None
|
||||||
|
for deviceUrl in lease_info.deviceUrl:
|
||||||
|
if deviceUrl.disk:
|
||||||
|
url = self._fix_esx_url(deviceUrl.url, host, port)
|
||||||
|
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
|
||||||
|
|
||||||
|
def _log_progress(self, progress):
|
||||||
|
"""Log data transfer progress."""
|
||||||
|
if (progress == 100 or (progress - self._last_logged_progress >=
|
||||||
|
MIN_PROGRESS_DIFF_TO_LOG)):
|
||||||
|
LOG.debug("Data transfer progress is %d%%.", progress)
|
||||||
|
self._last_logged_progress = progress
|
||||||
|
|
||||||
|
|
||||||
|
class FileWriteHandle(FileHandle):
|
||||||
|
"""Write handle for a file in VMware server."""
|
||||||
|
|
||||||
|
def __init__(self, host, port, data_center_name, datastore_name, cookies,
|
||||||
|
file_path, file_size, scheme='https', cacerts=False):
|
||||||
|
"""Initializes the write handle with given parameters.
|
||||||
|
|
||||||
|
:param host: ESX/VC server IP address or host name
|
||||||
|
:param port: port for connection
|
||||||
|
: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, port)
|
||||||
|
param_list = {'dcPath': data_center_name, 'dsName': datastore_name}
|
||||||
|
self._url = '%s/folder/%s' % (soap_url, file_path)
|
||||||
|
self._url = self._url + '?' + urlparse.urlencode(param_list)
|
||||||
|
|
||||||
|
self._conn = self._create_write_connection(self._url,
|
||||||
|
file_size,
|
||||||
|
cookies=cookies,
|
||||||
|
cacerts=cacerts)
|
||||||
|
FileHandle.__init__(self, self._conn)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Write data to the file.
|
||||||
|
|
||||||
|
:param data: data to be written
|
||||||
|
:raises: VimConnectionException, VimException
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._file_handle.send(data)
|
||||||
|
except requests.RequestException 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(_LW("Error occurred while reading the HTTP response."),
|
||||||
|
exc_info=True)
|
||||||
|
super(FileWriteHandle, self).close()
|
||||||
|
|
||||||
|
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, port, 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 or host name
|
||||||
|
:param port: port for connection
|
||||||
|
: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, port)
|
||||||
|
self._vm_ref = lease_info.entity
|
||||||
|
|
||||||
|
cookies = session.vim.client.options.transport.cookiejar
|
||||||
|
# Create HTTP connection to write to VMDK URL
|
||||||
|
octet_stream = 'binary/octet-stream'
|
||||||
|
self._conn = self._create_write_connection(self._url,
|
||||||
|
vmdk_size,
|
||||||
|
cookies=cookies,
|
||||||
|
overwrite='t',
|
||||||
|
content_type=octet_stream,
|
||||||
|
cacerts=session._cacert)
|
||||||
|
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 write(self, data):
|
||||||
|
"""Write data to the file.
|
||||||
|
|
||||||
|
:param data: data to be written
|
||||||
|
:raises: VimConnectionException, VimException
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._file_handle.send(data)
|
||||||
|
self._bytes_written += len(data)
|
||||||
|
except requests.RequestException 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)
|
||||||
|
|
||||||
|
# TODO(vbala) Move this method to FileHandle.
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
progress = int(float(self._bytes_written) / self._vmdk_size * 100)
|
||||||
|
self._log_progress(progress)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._session.invoke_api(self._session.vim,
|
||||||
|
'HttpNfcLeaseProgress',
|
||||||
|
self._lease,
|
||||||
|
percent=progress)
|
||||||
|
except exceptions.VimException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Error occurred while updating the "
|
||||||
|
"write progress of VMDK file with "
|
||||||
|
"URL = %s."),
|
||||||
|
self._url)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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(_LW("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, port, 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 or host name
|
||||||
|
:param port: port for connection
|
||||||
|
: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, port)
|
||||||
|
cookies = session.vim.client.options.transport.cookiejar
|
||||||
|
cacerts = session.vim.client.options.transport.verify
|
||||||
|
self._conn = self._create_read_connection(self._url,
|
||||||
|
cookies=cookies,
|
||||||
|
cacerts=cacerts)
|
||||||
|
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 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
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = self._file_handle.read(READ_CHUNKSIZE)
|
||||||
|
self._bytes_read += len(data)
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
progress = int(float(self._bytes_read) / self._vmdk_size * 100)
|
||||||
|
self._log_progress(progress)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._session.invoke_api(self._session.vim,
|
||||||
|
'HttpNfcLeaseProgress',
|
||||||
|
self._lease,
|
||||||
|
percent=progress)
|
||||||
|
except exceptions.VimException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Error occurred while updating the "
|
||||||
|
"read progress of VMDK file with URL = %s."),
|
||||||
|
self._url)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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(_LW("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 = next(self._iter)
|
||||||
|
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"
|
357
oslo_vmware/service.py
Normal file
357
oslo_vmware/service.py
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Common classes that provide access to vSphere services.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
import six.moves.http_client as httplib
|
||||||
|
import suds
|
||||||
|
from suds import cache
|
||||||
|
from suds import client
|
||||||
|
from suds import plugin
|
||||||
|
from suds import transport
|
||||||
|
|
||||||
|
from oslo.utils import timeutils
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
CACHE_TIMEOUT = 60 * 60 # One hour cache timeout
|
||||||
|
ADDRESS_IN_USE_ERROR = 'Address already in use'
|
||||||
|
CONN_ABORT_ERROR = 'Software caused connection abort'
|
||||||
|
RESP_NOT_XML_ERROR = 'Response is "text/html", not "text/xml"'
|
||||||
|
|
||||||
|
SERVICE_INSTANCE = 'ServiceInstance'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceMessagePlugin(plugin.MessagePlugin):
|
||||||
|
"""Suds plug-in handling some special cases while calling VI SDK."""
|
||||||
|
|
||||||
|
def add_attribute_for_value(self, node):
|
||||||
|
"""Helper to handle AnyType.
|
||||||
|
|
||||||
|
Suds does not handle AnyType properly. But VI SDK requires type
|
||||||
|
attribute to be set when AnyType is used.
|
||||||
|
|
||||||
|
:param node: XML value node
|
||||||
|
"""
|
||||||
|
if node.name == 'value':
|
||||||
|
node.set('xsi:type', 'xsd:string')
|
||||||
|
|
||||||
|
def marshalled(self, context):
|
||||||
|
"""Modifies the envelope document before it is sent.
|
||||||
|
|
||||||
|
This method provides the plug-in with the opportunity to prune empty
|
||||||
|
nodes and fix nodes before sending it to the server.
|
||||||
|
|
||||||
|
:param context: send context
|
||||||
|
"""
|
||||||
|
# Suds builds the entire request object based on the WSDL schema.
|
||||||
|
# VI SDK throws server errors if optional SOAP nodes are sent
|
||||||
|
# without values; e.g., <test/> as opposed to <test>test</test>.
|
||||||
|
context.envelope.prune()
|
||||||
|
context.envelope.walk(self.add_attribute_for_value)
|
||||||
|
|
||||||
|
|
||||||
|
class Response(six.BytesIO):
|
||||||
|
"""Response with an input stream as source."""
|
||||||
|
|
||||||
|
def __init__(self, stream, status=200, headers=None):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers or {}
|
||||||
|
self.reason = requests.status_codes._codes.get(
|
||||||
|
status, [''])[0].upper().replace('_', ' ')
|
||||||
|
six.BytesIO.__init__(self, stream)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _original_response(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def msg(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def read(self, chunk_size, **kwargs):
|
||||||
|
return six.BytesIO.read(self, chunk_size)
|
||||||
|
|
||||||
|
def info(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_all(self, name, default):
|
||||||
|
result = self.headers.get(name)
|
||||||
|
if not result:
|
||||||
|
return default
|
||||||
|
return [result]
|
||||||
|
|
||||||
|
def getheaders(self, name):
|
||||||
|
return self.get_all(name, [])
|
||||||
|
|
||||||
|
def release_conn(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class LocalFileAdapter(requests.adapters.HTTPAdapter):
|
||||||
|
"""Transport adapter for local files.
|
||||||
|
|
||||||
|
See http://stackoverflow.com/a/22989322
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _build_response_from_file(self, request):
|
||||||
|
file_path = request.url[7:]
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
buff = bytearray(os.path.getsize(file_path))
|
||||||
|
f.readinto(buff)
|
||||||
|
resp = Response(buff)
|
||||||
|
return self.build_response(request, resp)
|
||||||
|
|
||||||
|
def send(self, request, stream=False, timeout=None,
|
||||||
|
verify=True, cert=None, proxies=None):
|
||||||
|
return self._build_response_from_file(request)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestsTransport(transport.Transport):
|
||||||
|
def __init__(self, cacert=None, insecure=True):
|
||||||
|
transport.Transport.__init__(self)
|
||||||
|
# insecure flag is used only if cacert is not
|
||||||
|
# specified.
|
||||||
|
self.verify = cacert if cacert else not insecure
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.mount('file:///', LocalFileAdapter())
|
||||||
|
self.cookiejar = self.session.cookies
|
||||||
|
|
||||||
|
def open(self, request):
|
||||||
|
resp = self.session.get(request.url, verify=self.verify)
|
||||||
|
return six.StringIO(resp.content)
|
||||||
|
|
||||||
|
def send(self, request):
|
||||||
|
resp = self.session.post(request.url,
|
||||||
|
data=request.message,
|
||||||
|
headers=request.headers,
|
||||||
|
verify=self.verify)
|
||||||
|
return transport.Reply(resp.status_code, resp.headers, resp.content)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCache(cache.ObjectCache):
|
||||||
|
def __init__(self):
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
"""Retrieves the value for a key or None."""
|
||||||
|
now = timeutils.utcnow_ts()
|
||||||
|
for k in list(self._cache):
|
||||||
|
(timeout, _value) = self._cache[k]
|
||||||
|
if timeout and now >= timeout:
|
||||||
|
del self._cache[k]
|
||||||
|
|
||||||
|
return self._cache.get(key, (0, None))[1]
|
||||||
|
|
||||||
|
def put(self, key, value, time=CACHE_TIMEOUT):
|
||||||
|
"""Sets the value for a key."""
|
||||||
|
timeout = 0
|
||||||
|
if time != 0:
|
||||||
|
timeout = timeutils.utcnow_ts() + time
|
||||||
|
self._cache[key] = (timeout, value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_CACHE = MemoryCache()
|
||||||
|
|
||||||
|
|
||||||
|
class Service(object):
|
||||||
|
"""Base class containing common functionality for invoking vSphere
|
||||||
|
services
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, wsdl_url=None, soap_url=None,
|
||||||
|
cacert=None, insecure=True):
|
||||||
|
self.wsdl_url = wsdl_url
|
||||||
|
self.soap_url = soap_url
|
||||||
|
LOG.debug("Creating suds client with soap_url='%s' and wsdl_url='%s'",
|
||||||
|
self.soap_url, self.wsdl_url)
|
||||||
|
transport = RequestsTransport(cacert, insecure)
|
||||||
|
self.client = client.Client(self.wsdl_url,
|
||||||
|
transport=transport,
|
||||||
|
location=self.soap_url,
|
||||||
|
plugins=[ServiceMessagePlugin()],
|
||||||
|
cache=_CACHE)
|
||||||
|
self._service_content = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_base_url(protocol, host, port):
|
||||||
|
proto_str = '%s://' % protocol
|
||||||
|
host_str = '[%s]' % host if netaddr.valid_ipv6(host) else host
|
||||||
|
port_str = '' if port is None else ':%d' % port
|
||||||
|
return proto_str + host_str + port_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _retrieve_properties_ex_fault_checker(response):
|
||||||
|
"""Checks the RetrievePropertiesEx API response for errors.
|
||||||
|
|
||||||
|
Certain faults are sent in the SOAP body as a property of missingSet.
|
||||||
|
This method raises VimFaultException when a fault is found in the
|
||||||
|
response.
|
||||||
|
|
||||||
|
:param response: response from RetrievePropertiesEx API call
|
||||||
|
:raises: VimFaultException
|
||||||
|
"""
|
||||||
|
fault_list = []
|
||||||
|
details = {}
|
||||||
|
if not response:
|
||||||
|
# This is the case when the session has timed out. ESX SOAP
|
||||||
|
# server sends an empty RetrievePropertiesExResponse. Normally
|
||||||
|
# missingSet in the response objects has the specifics about
|
||||||
|
# the error, but that's not the case with a timed out idle
|
||||||
|
# session. It is as bad as a terminated session for we cannot
|
||||||
|
# use the session. Therefore setting fault to NotAuthenticated
|
||||||
|
# fault.
|
||||||
|
LOG.debug("RetrievePropertiesEx API response is empty; setting "
|
||||||
|
"fault to %s.",
|
||||||
|
exceptions.NOT_AUTHENTICATED)
|
||||||
|
fault_list = [exceptions.NOT_AUTHENTICATED]
|
||||||
|
else:
|
||||||
|
for obj_cont in response.objects:
|
||||||
|
if hasattr(obj_cont, 'missingSet'):
|
||||||
|
for missing_elem in obj_cont.missingSet:
|
||||||
|
f_type = missing_elem.fault.fault
|
||||||
|
f_name = f_type.__class__.__name__
|
||||||
|
fault_list.append(f_name)
|
||||||
|
if f_name == exceptions.NO_PERMISSION:
|
||||||
|
details['object'] = f_type.object.value
|
||||||
|
details['privilegeId'] = f_type.privilegeId
|
||||||
|
|
||||||
|
if fault_list:
|
||||||
|
fault_string = _("Error occurred while calling "
|
||||||
|
"RetrievePropertiesEx.")
|
||||||
|
raise exceptions.VimFaultException(fault_list,
|
||||||
|
fault_string,
|
||||||
|
details=details)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_content(self):
|
||||||
|
if self._service_content is None:
|
||||||
|
self._service_content = self.retrieve_service_content()
|
||||||
|
return self._service_content
|
||||||
|
|
||||||
|
def get_http_cookie(self):
|
||||||
|
"""Return the vCenter session cookie."""
|
||||||
|
cookies = self.client.options.transport.cookiejar
|
||||||
|
for cookie in cookies:
|
||||||
|
if cookie.name.lower() == 'vmware_soap_session':
|
||||||
|
return cookie.value
|
||||||
|
|
||||||
|
def __getattr__(self, attr_name):
|
||||||
|
"""Returns the method to invoke API identified by param attr_name."""
|
||||||
|
|
||||||
|
def request_handler(managed_object, **kwargs):
|
||||||
|
"""Handler for vSphere API calls.
|
||||||
|
|
||||||
|
Invokes the API and parses the response for fault checking and
|
||||||
|
other errors.
|
||||||
|
|
||||||
|
:param managed_object: managed object reference argument of the
|
||||||
|
API call
|
||||||
|
:param kwargs: keyword arguments of the API call
|
||||||
|
:returns: response of the API call
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if isinstance(managed_object, str):
|
||||||
|
# For strings, use string value for value and type
|
||||||
|
# of the managed object.
|
||||||
|
managed_object = vim_util.get_moref(managed_object,
|
||||||
|
managed_object)
|
||||||
|
if managed_object is None:
|
||||||
|
return
|
||||||
|
request = getattr(self.client.service, attr_name)
|
||||||
|
response = request(managed_object, **kwargs)
|
||||||
|
if (attr_name.lower() == 'retrievepropertiesex'):
|
||||||
|
Service._retrieve_properties_ex_fault_checker(response)
|
||||||
|
return response
|
||||||
|
except exceptions.VimFaultException:
|
||||||
|
# Catch the VimFaultException that is raised by the fault
|
||||||
|
# check of the SOAP response.
|
||||||
|
raise
|
||||||
|
|
||||||
|
except suds.WebFault as excep:
|
||||||
|
fault_string = None
|
||||||
|
if excep.fault:
|
||||||
|
fault_string = excep.fault.faultstring
|
||||||
|
|
||||||
|
doc = excep.document
|
||||||
|
detail = None
|
||||||
|
if doc is not None:
|
||||||
|
detail = doc.childAtPath('/detail')
|
||||||
|
if not detail:
|
||||||
|
# NOTE(arnaud): this is needed with VC 5.1
|
||||||
|
detail = doc.childAtPath('/Envelope/Body/Fault/detail')
|
||||||
|
fault_list = []
|
||||||
|
details = {}
|
||||||
|
if detail:
|
||||||
|
for fault in detail.getChildren():
|
||||||
|
fault_list.append(fault.get("type"))
|
||||||
|
for child in fault.getChildren():
|
||||||
|
details[child.name] = child.getText()
|
||||||
|
raise exceptions.VimFaultException(fault_list, fault_string,
|
||||||
|
excep, details)
|
||||||
|
|
||||||
|
except AttributeError as excep:
|
||||||
|
raise exceptions.VimAttributeException(
|
||||||
|
_("No such SOAP method %s.") % attr_name, excep)
|
||||||
|
|
||||||
|
except (httplib.CannotSendRequest,
|
||||||
|
httplib.ResponseNotReady,
|
||||||
|
httplib.CannotSendHeader) as excep:
|
||||||
|
raise exceptions.VimSessionOverLoadException(
|
||||||
|
_("httplib error in %s.") % attr_name, excep)
|
||||||
|
|
||||||
|
except requests.RequestException as excep:
|
||||||
|
raise exceptions.VimConnectionException(
|
||||||
|
_("requests error in %s.") % attr_name, excep)
|
||||||
|
|
||||||
|
except Exception as excep:
|
||||||
|
# TODO(vbala) should catch specific exceptions and raise
|
||||||
|
# appropriate VimExceptions.
|
||||||
|
|
||||||
|
# Socket errors which need special handling; some of these
|
||||||
|
# might be caused by server API call overload.
|
||||||
|
if (six.text_type(excep).find(ADDRESS_IN_USE_ERROR) != -1 or
|
||||||
|
six.text_type(excep).find(CONN_ABORT_ERROR)) != -1:
|
||||||
|
raise exceptions.VimSessionOverLoadException(
|
||||||
|
_("Socket error in %s.") % attr_name, excep)
|
||||||
|
# Type error which needs special handling; it might be caused
|
||||||
|
# by server API call overload.
|
||||||
|
elif six.text_type(excep).find(RESP_NOT_XML_ERROR) != -1:
|
||||||
|
raise exceptions.VimSessionOverLoadException(
|
||||||
|
_("Type error in %s.") % attr_name, excep)
|
||||||
|
else:
|
||||||
|
raise exceptions.VimException(
|
||||||
|
_("Exception in %s.") % attr_name, excep)
|
||||||
|
return request_handler
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "vSphere object"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "vSphere object"
|
11
oslo_vmware/tests/__init__.py
Normal file
11
oslo_vmware/tests/__init__.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 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.
|
53
oslo_vmware/tests/base.py
Normal file
53
oslo_vmware/tests/base.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Copyright 2010-2011 OpenStack Foundation
|
||||||
|
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 fixtures
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
_TRUE_VALUES = ('true', '1', 'yes')
|
||||||
|
|
||||||
|
# FIXME(dhellmann) Update this to use oslo.test library
|
||||||
|
|
||||||
|
|
||||||
|
class TestCase(testtools.TestCase):
|
||||||
|
|
||||||
|
"""Test case base class for all unit tests."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Run before each test method to initialize test environment."""
|
||||||
|
|
||||||
|
super(TestCase, self).setUp()
|
||||||
|
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
|
||||||
|
try:
|
||||||
|
test_timeout = int(test_timeout)
|
||||||
|
except ValueError:
|
||||||
|
# If timeout value is invalid do not set a timeout.
|
||||||
|
test_timeout = 0
|
||||||
|
if test_timeout > 0:
|
||||||
|
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
|
||||||
|
|
||||||
|
self.useFixture(fixtures.NestedTempfile())
|
||||||
|
self.useFixture(fixtures.TempHomeDir())
|
||||||
|
|
||||||
|
if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES:
|
||||||
|
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
||||||
|
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
|
||||||
|
if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES:
|
||||||
|
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
||||||
|
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||||
|
|
||||||
|
self.log_fixture = self.useFixture(fixtures.FakeLogger())
|
0
oslo_vmware/tests/objects/__init__.py
Normal file
0
oslo_vmware/tests/objects/__init__.py
Normal file
30
oslo_vmware/tests/objects/test_datacenter.py
Normal file
30
oslo_vmware/tests/objects/test_datacenter.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2014 VMware, Inc.
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from oslo_vmware.objects import datacenter
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class DatacenterTestCase(base.TestCase):
|
||||||
|
|
||||||
|
"""Test the Datacenter object."""
|
||||||
|
|
||||||
|
def test_dc(self):
|
||||||
|
self.assertRaises(ValueError, datacenter.Datacenter, None, 'dc-1')
|
||||||
|
self.assertRaises(ValueError, datacenter.Datacenter, mock.Mock(), None)
|
||||||
|
dc = datacenter.Datacenter('ref', 'name')
|
||||||
|
self.assertEqual('ref', dc.ref)
|
||||||
|
self.assertEqual('name', dc.name)
|
384
oslo_vmware/tests/objects/test_datastore.py
Normal file
384
oslo_vmware/tests/objects/test_datastore.py
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
# Copyright (c) 2014 VMware, Inc.
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from oslo.utils import units
|
||||||
|
from oslo_vmware import constants
|
||||||
|
from oslo_vmware.objects import datastore
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
class HostMount(object):
|
||||||
|
|
||||||
|
def __init__(self, key, mountInfo):
|
||||||
|
self.key = key
|
||||||
|
self.mountInfo = mountInfo
|
||||||
|
|
||||||
|
|
||||||
|
class MountInfo(object):
|
||||||
|
|
||||||
|
def __init__(self, accessMode, mounted, accessible):
|
||||||
|
self.accessMode = accessMode
|
||||||
|
self.mounted = mounted
|
||||||
|
self.accessible = accessible
|
||||||
|
|
||||||
|
|
||||||
|
class DatastoreTestCase(base.TestCase):
|
||||||
|
|
||||||
|
"""Test the Datastore object."""
|
||||||
|
|
||||||
|
def test_ds(self):
|
||||||
|
ds = datastore.Datastore(
|
||||||
|
"fake_ref", "ds_name", 2 * units.Gi, 1 * units.Gi)
|
||||||
|
self.assertEqual('ds_name', ds.name)
|
||||||
|
self.assertEqual('fake_ref', ds.ref)
|
||||||
|
self.assertEqual(2 * units.Gi, ds.capacity)
|
||||||
|
self.assertEqual(1 * units.Gi, ds.freespace)
|
||||||
|
|
||||||
|
def test_ds_invalid_space(self):
|
||||||
|
self.assertRaises(ValueError, datastore.Datastore,
|
||||||
|
"fake_ref", "ds_name", 1 * units.Gi, 2 * units.Gi)
|
||||||
|
self.assertRaises(ValueError, datastore.Datastore,
|
||||||
|
"fake_ref", "ds_name", None, 2 * units.Gi)
|
||||||
|
|
||||||
|
def test_ds_no_capacity_no_freespace(self):
|
||||||
|
ds = datastore.Datastore("fake_ref", "ds_name")
|
||||||
|
self.assertIsNone(ds.capacity)
|
||||||
|
self.assertIsNone(ds.freespace)
|
||||||
|
|
||||||
|
def test_ds_invalid(self):
|
||||||
|
self.assertRaises(ValueError, datastore.Datastore, None, "ds_name")
|
||||||
|
self.assertRaises(ValueError, datastore.Datastore, "fake_ref", None)
|
||||||
|
|
||||||
|
def test_build_path(self):
|
||||||
|
ds = datastore.Datastore("fake_ref", "ds_name")
|
||||||
|
ds_path = ds.build_path("some_dir", "foo.vmdk")
|
||||||
|
self.assertEqual('[ds_name] some_dir/foo.vmdk', str(ds_path))
|
||||||
|
|
||||||
|
def test_build_url(self):
|
||||||
|
ds = datastore.Datastore("fake_ref", "ds_name")
|
||||||
|
path = 'images/ubuntu.vmdk'
|
||||||
|
self.assertRaises(ValueError, ds.build_url, 'https', '10.0.0.2', path)
|
||||||
|
ds.datacenter = mock.Mock()
|
||||||
|
ds.datacenter.name = "dc_path"
|
||||||
|
ds_url = ds.build_url('https', '10.0.0.2', path)
|
||||||
|
self.assertEqual(ds_url.datastore_name, "ds_name")
|
||||||
|
self.assertEqual(ds_url.datacenter_path, "dc_path")
|
||||||
|
self.assertEqual(ds_url.path, path)
|
||||||
|
|
||||||
|
def test_get_summary(self):
|
||||||
|
ds_ref = vim_util.get_moref('ds-0', 'Datastore')
|
||||||
|
ds = datastore.Datastore(ds_ref, 'ds-name')
|
||||||
|
summary = mock.sentinel.summary
|
||||||
|
session = mock.Mock()
|
||||||
|
session.invoke_api = mock.Mock()
|
||||||
|
session.invoke_api.return_value = summary
|
||||||
|
ret = ds.get_summary(session)
|
||||||
|
self.assertEqual(summary, ret)
|
||||||
|
session.invoke_api.assert_called_once_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
session.vim,
|
||||||
|
ds.ref, 'summary')
|
||||||
|
|
||||||
|
def test_get_connected_hosts(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
ds_ref = vim_util.get_moref('ds-0', 'Datastore')
|
||||||
|
ds = datastore.Datastore(ds_ref, 'ds-name')
|
||||||
|
ds.get_summary = mock.Mock()
|
||||||
|
ds.get_summary.return_value.accessible = False
|
||||||
|
self.assertEqual([], ds.get_connected_hosts(session))
|
||||||
|
ds.get_summary.return_value.accessible = True
|
||||||
|
m1 = HostMount("m1", MountInfo('readWrite', True, True))
|
||||||
|
m2 = HostMount("m2", MountInfo('read', True, True))
|
||||||
|
m3 = HostMount("m3", MountInfo('readWrite', False, True))
|
||||||
|
m4 = HostMount("m4", MountInfo('readWrite', True, False))
|
||||||
|
ds.get_summary.assert_called_once_with(session)
|
||||||
|
|
||||||
|
class Prop(object):
|
||||||
|
DatastoreHostMount = [m1, m2, m3, m4]
|
||||||
|
session.invoke_api = mock.Mock()
|
||||||
|
session.invoke_api.return_value = Prop()
|
||||||
|
hosts = ds.get_connected_hosts(session)
|
||||||
|
self.assertEqual(1, len(hosts))
|
||||||
|
self.assertEqual("m1", hosts.pop())
|
||||||
|
|
||||||
|
def test_is_datastore_mount_usable(self):
|
||||||
|
m = MountInfo('readWrite', True, True)
|
||||||
|
self.assertTrue(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('read', True, True)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('readWrite', False, True)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('readWrite', True, False)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('readWrite', False, False)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('readWrite', None, None)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
m = MountInfo('readWrite', None, True)
|
||||||
|
self.assertFalse(datastore.Datastore.is_datastore_mount_usable(m))
|
||||||
|
|
||||||
|
|
||||||
|
class DatastorePathTestCase(base.TestCase):
|
||||||
|
|
||||||
|
"""Test the DatastorePath object."""
|
||||||
|
|
||||||
|
def test_ds_path(self):
|
||||||
|
p = datastore.DatastorePath('dsname', 'a/b/c', 'file.iso')
|
||||||
|
self.assertEqual('[dsname] a/b/c/file.iso', str(p))
|
||||||
|
self.assertEqual('a/b/c/file.iso', p.rel_path)
|
||||||
|
self.assertEqual('a/b/c', p.parent.rel_path)
|
||||||
|
self.assertEqual('[dsname] a/b/c', str(p.parent))
|
||||||
|
self.assertEqual('dsname', p.datastore)
|
||||||
|
self.assertEqual('file.iso', p.basename)
|
||||||
|
self.assertEqual('a/b/c', p.dirname)
|
||||||
|
|
||||||
|
def test_ds_path_no_ds_name(self):
|
||||||
|
bad_args = [
|
||||||
|
('', ['a/b/c', 'file.iso']),
|
||||||
|
(None, ['a/b/c', 'file.iso'])]
|
||||||
|
for t in bad_args:
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError, datastore.DatastorePath,
|
||||||
|
t[0], *t[1])
|
||||||
|
|
||||||
|
def test_ds_path_invalid_path_components(self):
|
||||||
|
bad_args = [
|
||||||
|
('dsname', [None]),
|
||||||
|
('dsname', ['', None]),
|
||||||
|
('dsname', ['a', None]),
|
||||||
|
('dsname', ['a', None, 'b']),
|
||||||
|
('dsname', [None, '']),
|
||||||
|
('dsname', [None, 'b'])]
|
||||||
|
|
||||||
|
for t in bad_args:
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError, datastore.DatastorePath,
|
||||||
|
t[0], *t[1])
|
||||||
|
|
||||||
|
def test_ds_path_no_subdir(self):
|
||||||
|
args = [
|
||||||
|
('dsname', ['', 'x.vmdk']),
|
||||||
|
('dsname', ['x.vmdk'])]
|
||||||
|
|
||||||
|
canonical_p = datastore.DatastorePath('dsname', 'x.vmdk')
|
||||||
|
self.assertEqual('[dsname] x.vmdk', str(canonical_p))
|
||||||
|
self.assertEqual('', canonical_p.dirname)
|
||||||
|
self.assertEqual('x.vmdk', canonical_p.basename)
|
||||||
|
self.assertEqual('x.vmdk', canonical_p.rel_path)
|
||||||
|
for t in args:
|
||||||
|
p = datastore.DatastorePath(t[0], *t[1])
|
||||||
|
self.assertEqual(str(canonical_p), str(p))
|
||||||
|
|
||||||
|
def test_ds_path_ds_only(self):
|
||||||
|
args = [
|
||||||
|
('dsname', []),
|
||||||
|
('dsname', ['']),
|
||||||
|
('dsname', ['', ''])]
|
||||||
|
|
||||||
|
canonical_p = datastore.DatastorePath('dsname')
|
||||||
|
self.assertEqual('[dsname]', str(canonical_p))
|
||||||
|
self.assertEqual('', canonical_p.rel_path)
|
||||||
|
self.assertEqual('', canonical_p.basename)
|
||||||
|
self.assertEqual('', canonical_p.dirname)
|
||||||
|
for t in args:
|
||||||
|
p = datastore.DatastorePath(t[0], *t[1])
|
||||||
|
self.assertEqual(str(canonical_p), str(p))
|
||||||
|
self.assertEqual(canonical_p.rel_path, p.rel_path)
|
||||||
|
|
||||||
|
def test_ds_path_equivalence(self):
|
||||||
|
args = [
|
||||||
|
('dsname', ['a/b/c/', 'x.vmdk']),
|
||||||
|
('dsname', ['a/', 'b/c/', 'x.vmdk']),
|
||||||
|
('dsname', ['a', 'b', 'c', 'x.vmdk']),
|
||||||
|
('dsname', ['a/b/c', 'x.vmdk'])]
|
||||||
|
|
||||||
|
canonical_p = datastore.DatastorePath('dsname', 'a/b/c', 'x.vmdk')
|
||||||
|
for t in args:
|
||||||
|
p = datastore.DatastorePath(t[0], *t[1])
|
||||||
|
self.assertEqual(str(canonical_p), str(p))
|
||||||
|
self.assertEqual(canonical_p.datastore, p.datastore)
|
||||||
|
self.assertEqual(canonical_p.rel_path, p.rel_path)
|
||||||
|
self.assertEqual(str(canonical_p.parent), str(p.parent))
|
||||||
|
|
||||||
|
def test_ds_path_non_equivalence(self):
|
||||||
|
args = [
|
||||||
|
# leading slash
|
||||||
|
('dsname', ['/a', 'b', 'c', 'x.vmdk']),
|
||||||
|
('dsname', ['/a/b/c/', 'x.vmdk']),
|
||||||
|
('dsname', ['a/b/c', '/x.vmdk']),
|
||||||
|
# leading space
|
||||||
|
('dsname', ['a/b/c/', ' x.vmdk']),
|
||||||
|
('dsname', ['a/', ' b/c/', 'x.vmdk']),
|
||||||
|
('dsname', [' a', 'b', 'c', 'x.vmdk']),
|
||||||
|
# trailing space
|
||||||
|
('dsname', ['/a/b/c/', 'x.vmdk ']),
|
||||||
|
('dsname', ['a/b/c/ ', 'x.vmdk'])]
|
||||||
|
|
||||||
|
canonical_p = datastore.DatastorePath('dsname', 'a/b/c', 'x.vmdk')
|
||||||
|
for t in args:
|
||||||
|
p = datastore.DatastorePath(t[0], *t[1])
|
||||||
|
self.assertNotEqual(str(canonical_p), str(p))
|
||||||
|
|
||||||
|
def test_equal(self):
|
||||||
|
a = datastore.DatastorePath('ds_name', 'a')
|
||||||
|
b = datastore.DatastorePath('ds_name', 'a')
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_join(self):
|
||||||
|
p = datastore.DatastorePath('ds_name', 'a')
|
||||||
|
ds_path = p.join('b')
|
||||||
|
self.assertEqual('[ds_name] a/b', str(ds_path))
|
||||||
|
|
||||||
|
p = datastore.DatastorePath('ds_name', 'a')
|
||||||
|
ds_path = p.join()
|
||||||
|
bad_args = [
|
||||||
|
[None],
|
||||||
|
['', None],
|
||||||
|
['a', None],
|
||||||
|
['a', None, 'b']]
|
||||||
|
for arg in bad_args:
|
||||||
|
self.assertRaises(ValueError, p.join, *arg)
|
||||||
|
|
||||||
|
def test_ds_path_parse(self):
|
||||||
|
p = datastore.DatastorePath.parse('[dsname]')
|
||||||
|
self.assertEqual('dsname', p.datastore)
|
||||||
|
self.assertEqual('', p.rel_path)
|
||||||
|
|
||||||
|
p = datastore.DatastorePath.parse('[dsname] folder')
|
||||||
|
self.assertEqual('dsname', p.datastore)
|
||||||
|
self.assertEqual('folder', p.rel_path)
|
||||||
|
|
||||||
|
p = datastore.DatastorePath.parse('[dsname] folder/file')
|
||||||
|
self.assertEqual('dsname', p.datastore)
|
||||||
|
self.assertEqual('folder/file', p.rel_path)
|
||||||
|
|
||||||
|
for p in [None, '']:
|
||||||
|
self.assertRaises(ValueError, datastore.DatastorePath.parse, p)
|
||||||
|
|
||||||
|
for p in ['bad path', '/a/b/c', 'a/b/c']:
|
||||||
|
self.assertRaises(IndexError, datastore.DatastorePath.parse, p)
|
||||||
|
|
||||||
|
|
||||||
|
class DatastoreURLTestCase(base.TestCase):
|
||||||
|
|
||||||
|
"""Test the DatastoreURL object."""
|
||||||
|
|
||||||
|
def test_path_strip(self):
|
||||||
|
scheme = 'https'
|
||||||
|
server = '13.37.73.31'
|
||||||
|
path = 'images/ubuntu-14.04.vmdk'
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = datastore.DatastoreURL(scheme, server, path, dc_path, ds_name)
|
||||||
|
expected_url = '%s://%s/folder/%s?%s' % (
|
||||||
|
scheme, server, path, query)
|
||||||
|
self.assertEqual(expected_url, str(url))
|
||||||
|
|
||||||
|
def test_path_lstrip(self):
|
||||||
|
scheme = 'https'
|
||||||
|
server = '13.37.73.31'
|
||||||
|
path = '/images/ubuntu-14.04.vmdk'
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = datastore.DatastoreURL(scheme, server, path, dc_path, ds_name)
|
||||||
|
expected_url = '%s://%s/folder/%s?%s' % (
|
||||||
|
scheme, server, path.lstrip('/'), query)
|
||||||
|
self.assertEqual(expected_url, str(url))
|
||||||
|
|
||||||
|
def test_path_rstrip(self):
|
||||||
|
scheme = 'https'
|
||||||
|
server = '13.37.73.31'
|
||||||
|
path = 'images/ubuntu-14.04.vmdk/'
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = datastore.DatastoreURL(scheme, server, path, dc_path, ds_name)
|
||||||
|
expected_url = '%s://%s/folder/%s?%s' % (
|
||||||
|
scheme, server, path.rstrip('/'), query)
|
||||||
|
self.assertEqual(expected_url, str(url))
|
||||||
|
|
||||||
|
def test_urlparse(self):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/images/aa.vmdk?%s' % query
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
self.assertEqual(url, str(ds_url))
|
||||||
|
|
||||||
|
def test_datastore_name(self):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/images/aa.vmdk?%s' % query
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
self.assertEqual(ds_name, ds_url.datastore_name)
|
||||||
|
|
||||||
|
def test_datacenter_path(self):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/images/aa.vmdk?%s' % query
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
self.assertEqual(dc_path, ds_url.datacenter_path)
|
||||||
|
|
||||||
|
def test_path(self):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
path = 'images/aa.vmdk'
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/%s?%s' % (path, query)
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
self.assertEqual(path, ds_url.path)
|
||||||
|
|
||||||
|
@mock.patch('six.moves.http_client.HTTPSConnection')
|
||||||
|
def test_connect(self, mock_conn):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/images/aa.vmdk?%s' % query
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
cookie = mock.Mock()
|
||||||
|
ds_url.connect('PUT', 128, cookie)
|
||||||
|
mock_conn.assert_called_once_with('13.37.73.31')
|
||||||
|
|
||||||
|
def test_get_transfer_ticket(self):
|
||||||
|
dc_path = 'datacenter-1'
|
||||||
|
ds_name = 'datastore-1'
|
||||||
|
params = {'dcPath': dc_path, 'dsName': ds_name}
|
||||||
|
query = urlparse.urlencode(params)
|
||||||
|
url = 'https://13.37.73.31/folder/images/aa.vmdk?%s' % query
|
||||||
|
session = mock.Mock()
|
||||||
|
session.invoke_api = mock.Mock()
|
||||||
|
|
||||||
|
class Ticket(object):
|
||||||
|
id = 'fake_id'
|
||||||
|
session.invoke_api.return_value = Ticket()
|
||||||
|
ds_url = datastore.DatastoreURL.urlparse(url)
|
||||||
|
ticket = ds_url.get_transfer_ticket(session, 'PUT')
|
||||||
|
self.assertEqual('%s="%s"' % (constants.CGI_COOKIE_KEY, 'fake_id'),
|
||||||
|
ticket)
|
549
oslo_vmware/tests/test_api.py
Normal file
549
oslo_vmware/tests/test_api.py
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
# 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 session management and API invocation classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from eventlet import greenthread
|
||||||
|
import mock
|
||||||
|
import six
|
||||||
|
import suds
|
||||||
|
|
||||||
|
from oslo_vmware import api
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import pbm
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
class RetryDecoratorTest(base.TestCase):
|
||||||
|
"""Tests for retry decorator class."""
|
||||||
|
|
||||||
|
def test_retry(self):
|
||||||
|
result = "RESULT"
|
||||||
|
|
||||||
|
@api.RetryDecorator()
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
return result
|
||||||
|
|
||||||
|
self.assertEqual(result, func())
|
||||||
|
|
||||||
|
def func2(*args, **kwargs):
|
||||||
|
return result
|
||||||
|
|
||||||
|
retry = api.RetryDecorator()
|
||||||
|
self.assertEqual(result, retry(func2)())
|
||||||
|
self.assertTrue(retry._retry_count == 0)
|
||||||
|
|
||||||
|
def test_retry_with_expected_exceptions(self):
|
||||||
|
result = "RESULT"
|
||||||
|
responses = [exceptions.VimSessionOverLoadException(None),
|
||||||
|
exceptions.VimSessionOverLoadException(None),
|
||||||
|
result]
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
response = responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
sleep_time_incr = 0.01
|
||||||
|
retry_count = 2
|
||||||
|
retry = api.RetryDecorator(10, sleep_time_incr, 10,
|
||||||
|
(exceptions.VimSessionOverLoadException,))
|
||||||
|
self.assertEqual(result, retry(func)())
|
||||||
|
self.assertTrue(retry._retry_count == retry_count)
|
||||||
|
self.assertEqual(retry_count * sleep_time_incr, retry._sleep_time)
|
||||||
|
|
||||||
|
def test_retry_with_max_retries(self):
|
||||||
|
responses = [exceptions.VimSessionOverLoadException(None),
|
||||||
|
exceptions.VimSessionOverLoadException(None),
|
||||||
|
exceptions.VimSessionOverLoadException(None)]
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
response = responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
retry = api.RetryDecorator(2, 0, 0,
|
||||||
|
(exceptions.VimSessionOverLoadException,))
|
||||||
|
self.assertRaises(exceptions.VimSessionOverLoadException, retry(func))
|
||||||
|
self.assertTrue(retry._retry_count == 2)
|
||||||
|
|
||||||
|
def test_retry_with_unexpected_exception(self):
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
raise exceptions.VimException(None)
|
||||||
|
|
||||||
|
retry = api.RetryDecorator()
|
||||||
|
self.assertRaises(exceptions.VimException, retry(func))
|
||||||
|
self.assertTrue(retry._retry_count == 0)
|
||||||
|
|
||||||
|
|
||||||
|
class VMwareAPISessionTest(base.TestCase):
|
||||||
|
"""Tests for VMwareAPISession."""
|
||||||
|
|
||||||
|
SERVER_IP = '10.1.2.3'
|
||||||
|
PORT = 443
|
||||||
|
USERNAME = 'admin'
|
||||||
|
PASSWORD = 'password'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(VMwareAPISessionTest, self).setUp()
|
||||||
|
patcher = mock.patch('oslo_vmware.vim.Vim')
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.VimMock = patcher.start()
|
||||||
|
self.VimMock.side_effect = lambda *args, **kw: mock.MagicMock()
|
||||||
|
self.cert_mock = mock.Mock()
|
||||||
|
|
||||||
|
def _create_api_session(self, _create_session, retry_count=10,
|
||||||
|
task_poll_interval=1):
|
||||||
|
return api.VMwareAPISession(VMwareAPISessionTest.SERVER_IP,
|
||||||
|
VMwareAPISessionTest.USERNAME,
|
||||||
|
VMwareAPISessionTest.PASSWORD,
|
||||||
|
retry_count,
|
||||||
|
task_poll_interval,
|
||||||
|
'https',
|
||||||
|
_create_session,
|
||||||
|
port=VMwareAPISessionTest.PORT,
|
||||||
|
cacert=self.cert_mock,
|
||||||
|
insecure=False)
|
||||||
|
|
||||||
|
def test_vim(self):
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
api_session.vim
|
||||||
|
self.VimMock.assert_called_with(protocol=api_session._scheme,
|
||||||
|
host=VMwareAPISessionTest.SERVER_IP,
|
||||||
|
port=VMwareAPISessionTest.PORT,
|
||||||
|
wsdl_url=api_session._vim_wsdl_loc,
|
||||||
|
cacert=self.cert_mock,
|
||||||
|
insecure=False)
|
||||||
|
|
||||||
|
@mock.patch.object(pbm, 'Pbm')
|
||||||
|
def test_pbm(self, pbm_mock):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
cookie = mock.Mock()
|
||||||
|
vim_obj.get_http_cookie.return_value = cookie
|
||||||
|
api_session._pbm_wsdl_loc = mock.Mock()
|
||||||
|
|
||||||
|
pbm = mock.Mock()
|
||||||
|
pbm_mock.return_value = pbm
|
||||||
|
api_session._get_session_cookie = mock.Mock(return_value=cookie)
|
||||||
|
|
||||||
|
self.assertEqual(pbm, api_session.pbm)
|
||||||
|
pbm.set_soap_cookie.assert_called_once_with(cookie)
|
||||||
|
|
||||||
|
def test_create_session(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.key = "12345"
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
cookie = mock.Mock()
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.Login.return_value = session
|
||||||
|
vim_obj.get_http_cookie.return_value = cookie
|
||||||
|
|
||||||
|
pbm = mock.Mock()
|
||||||
|
api_session._pbm = pbm
|
||||||
|
|
||||||
|
api_session._create_session()
|
||||||
|
session_manager = vim_obj.service_content.sessionManager
|
||||||
|
vim_obj.Login.assert_called_once_with(
|
||||||
|
session_manager, userName=VMwareAPISessionTest.USERNAME,
|
||||||
|
password=VMwareAPISessionTest.PASSWORD)
|
||||||
|
self.assertFalse(vim_obj.TerminateSession.called)
|
||||||
|
self.assertEqual(session.key, api_session._session_id)
|
||||||
|
pbm.set_soap_cookie.assert_called_once_with(cookie)
|
||||||
|
|
||||||
|
def test_create_session_with_existing_session(self):
|
||||||
|
old_session_key = '12345'
|
||||||
|
new_session_key = '67890'
|
||||||
|
session = mock.Mock()
|
||||||
|
session.key = new_session_key
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
api_session._session_id = old_session_key
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.Login.return_value = session
|
||||||
|
|
||||||
|
api_session._create_session()
|
||||||
|
session_manager = vim_obj.service_content.sessionManager
|
||||||
|
vim_obj.Login.assert_called_once_with(
|
||||||
|
session_manager, userName=VMwareAPISessionTest.USERNAME,
|
||||||
|
password=VMwareAPISessionTest.PASSWORD)
|
||||||
|
vim_obj.TerminateSession.assert_called_once_with(
|
||||||
|
session_manager, sessionId=[old_session_key])
|
||||||
|
self.assertEqual(new_session_key, api_session._session_id)
|
||||||
|
|
||||||
|
def test_invoke_api(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
response = mock.Mock()
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
return response
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
ret = api_session.invoke_api(module, 'api')
|
||||||
|
self.assertEqual(response, ret)
|
||||||
|
|
||||||
|
def test_logout_with_exception(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.key = "12345"
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.Login.return_value = session
|
||||||
|
vim_obj.Logout.side_effect = exceptions.VimFaultException([], None)
|
||||||
|
api_session._create_session()
|
||||||
|
api_session.logout()
|
||||||
|
self.assertEqual("12345", api_session._session_id)
|
||||||
|
|
||||||
|
def test_logout_no_session(self):
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
api_session.logout()
|
||||||
|
self.assertEqual(0, vim_obj.Logout.call_count)
|
||||||
|
|
||||||
|
def test_logout_calls_vim_logout(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.key = "12345"
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.Login.return_value = session
|
||||||
|
vim_obj.Logout.return_value = None
|
||||||
|
|
||||||
|
api_session._create_session()
|
||||||
|
session_manager = vim_obj.service_content.sessionManager
|
||||||
|
vim_obj.Login.assert_called_once_with(
|
||||||
|
session_manager, userName=VMwareAPISessionTest.USERNAME,
|
||||||
|
password=VMwareAPISessionTest.PASSWORD)
|
||||||
|
api_session.logout()
|
||||||
|
vim_obj.Logout.assert_called_once_with(
|
||||||
|
session_manager)
|
||||||
|
self.assertIsNone(api_session._session_id)
|
||||||
|
|
||||||
|
def test_invoke_api_with_expected_exception(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
api_session._create_session = mock.Mock()
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.SessionIsActive.return_value = False
|
||||||
|
ret = mock.Mock()
|
||||||
|
responses = [exceptions.VimConnectionException(None), ret]
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
response = responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
self.assertEqual(ret, api_session.invoke_api(module, 'api'))
|
||||||
|
api_session._create_session.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_invoke_api_not_recreate_session(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
api_session._create_session = mock.Mock()
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.SessionIsActive.return_value = True
|
||||||
|
ret = mock.Mock()
|
||||||
|
responses = [exceptions.VimConnectionException(None), ret]
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
response = responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
self.assertEqual(ret, api_session.invoke_api(module, 'api'))
|
||||||
|
self.assertFalse(api_session._create_session.called)
|
||||||
|
|
||||||
|
def test_invoke_api_with_vim_fault_exception(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
raise exceptions.VimFaultException([], None)
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
self.assertRaises(exceptions.VimFaultException,
|
||||||
|
api_session.invoke_api,
|
||||||
|
module,
|
||||||
|
'api')
|
||||||
|
|
||||||
|
def test_invoke_api_with_vim_fault_exception_details(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
fault_string = 'Invalid property.'
|
||||||
|
fault_list = [exceptions.INVALID_PROPERTY]
|
||||||
|
details = {u'name': suds.sax.text.Text(u'фира')}
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api.side_effect = exceptions.VimFaultException(fault_list,
|
||||||
|
fault_string,
|
||||||
|
details=details)
|
||||||
|
e = self.assertRaises(exceptions.InvalidPropertyException,
|
||||||
|
api_session.invoke_api,
|
||||||
|
module,
|
||||||
|
'api')
|
||||||
|
details_str = u"{'name': 'фира'}"
|
||||||
|
expected_str = "%s\nFaults: %s\nDetails: %s" % (fault_string,
|
||||||
|
fault_list,
|
||||||
|
details_str)
|
||||||
|
self.assertEqual(expected_str, six.text_type(e))
|
||||||
|
self.assertEqual(details, e.details)
|
||||||
|
|
||||||
|
def test_invoke_api_with_empty_response(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.SessionIsActive.return_value = True
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
raise exceptions.VimFaultException(
|
||||||
|
[exceptions.NOT_AUTHENTICATED], None)
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
ret = api_session.invoke_api(module, 'api')
|
||||||
|
self.assertEqual([], ret)
|
||||||
|
vim_obj.SessionIsActive.assert_called_once_with(
|
||||||
|
vim_obj.service_content.sessionManager,
|
||||||
|
sessionID=api_session._session_id,
|
||||||
|
userName=api_session._session_username)
|
||||||
|
|
||||||
|
def test_invoke_api_with_stale_session(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
api_session._create_session = mock.Mock()
|
||||||
|
vim_obj = api_session.vim
|
||||||
|
vim_obj.SessionIsActive.return_value = False
|
||||||
|
result = mock.Mock()
|
||||||
|
responses = [exceptions.VimFaultException(
|
||||||
|
[exceptions.NOT_AUTHENTICATED], None), result]
|
||||||
|
|
||||||
|
def api(*args, **kwargs):
|
||||||
|
response = responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
module = mock.Mock()
|
||||||
|
module.api = api
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
ret = api_session.invoke_api(module, 'api')
|
||||||
|
self.assertEqual(result, ret)
|
||||||
|
vim_obj.SessionIsActive.assert_called_once_with(
|
||||||
|
vim_obj.service_content.sessionManager,
|
||||||
|
sessionID=api_session._session_id,
|
||||||
|
userName=api_session._session_username)
|
||||||
|
api_session._create_session.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_wait_for_task(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
task_info_list = [('queued', 0), ('running', 40), ('success', 100)]
|
||||||
|
task_info_list_size = len(task_info_list)
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
(state, progress) = task_info_list.pop(0)
|
||||||
|
task_info = mock.Mock()
|
||||||
|
task_info.progress = progress
|
||||||
|
task_info.state = state
|
||||||
|
return task_info
|
||||||
|
|
||||||
|
api_session.invoke_api = mock.Mock(side_effect=invoke_api_side_effect)
|
||||||
|
task = mock.Mock()
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
ret = api_session.wait_for_task(task)
|
||||||
|
self.assertEqual('success', ret.state)
|
||||||
|
self.assertEqual(100, ret.progress)
|
||||||
|
api_session.invoke_api.assert_called_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
api_session.vim, task,
|
||||||
|
'info')
|
||||||
|
self.assertEqual(task_info_list_size,
|
||||||
|
api_session.invoke_api.call_count)
|
||||||
|
|
||||||
|
def test_wait_for_task_with_error_state(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
task_info_list = [('queued', 0), ('running', 40), ('error', -1)]
|
||||||
|
task_info_list_size = len(task_info_list)
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
(state, progress) = task_info_list.pop(0)
|
||||||
|
task_info = mock.Mock()
|
||||||
|
task_info.progress = progress
|
||||||
|
task_info.state = state
|
||||||
|
return task_info
|
||||||
|
|
||||||
|
api_session.invoke_api = mock.Mock(side_effect=invoke_api_side_effect)
|
||||||
|
task = mock.Mock()
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
self.assertRaises(exceptions.VMwareDriverException,
|
||||||
|
api_session.wait_for_task,
|
||||||
|
task)
|
||||||
|
api_session.invoke_api.assert_called_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
api_session.vim, task,
|
||||||
|
'info')
|
||||||
|
self.assertEqual(task_info_list_size,
|
||||||
|
api_session.invoke_api.call_count)
|
||||||
|
|
||||||
|
def test_wait_for_task_with_invoke_api_exception(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
api_session.invoke_api = mock.Mock(
|
||||||
|
side_effect=exceptions.VimException(None))
|
||||||
|
task = mock.Mock()
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
self.assertRaises(exceptions.VimException,
|
||||||
|
api_session.wait_for_task,
|
||||||
|
task)
|
||||||
|
api_session.invoke_api.assert_called_once_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
api_session.vim, task,
|
||||||
|
'info')
|
||||||
|
|
||||||
|
def test_wait_for_lease_ready(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
lease_states = ['initializing', 'ready']
|
||||||
|
num_states = len(lease_states)
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
return lease_states.pop(0)
|
||||||
|
|
||||||
|
api_session.invoke_api = mock.Mock(side_effect=invoke_api_side_effect)
|
||||||
|
lease = mock.Mock()
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
api_session.wait_for_lease_ready(lease)
|
||||||
|
api_session.invoke_api.assert_called_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
api_session.vim, lease,
|
||||||
|
'state')
|
||||||
|
self.assertEqual(num_states, api_session.invoke_api.call_count)
|
||||||
|
|
||||||
|
def test_wait_for_lease_ready_with_error_state(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
responses = ['initializing', 'error', 'error_msg']
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
return responses.pop(0)
|
||||||
|
|
||||||
|
api_session.invoke_api = mock.Mock(side_effect=invoke_api_side_effect)
|
||||||
|
lease = mock.Mock()
|
||||||
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
|
self.assertRaises(exceptions.VimException,
|
||||||
|
api_session.wait_for_lease_ready,
|
||||||
|
lease)
|
||||||
|
exp_calls = [mock.call(vim_util, 'get_object_property',
|
||||||
|
api_session.vim, lease, 'state')] * 2
|
||||||
|
exp_calls.append(mock.call(vim_util, 'get_object_property',
|
||||||
|
api_session.vim, lease, 'error'))
|
||||||
|
self.assertEqual(exp_calls, api_session.invoke_api.call_args_list)
|
||||||
|
|
||||||
|
def test_wait_for_lease_ready_with_unknown_state(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
api_session.invoke_api = mock.Mock(side_effect=invoke_api_side_effect)
|
||||||
|
lease = mock.Mock()
|
||||||
|
self.assertRaises(exceptions.VimException,
|
||||||
|
api_session.wait_for_lease_ready,
|
||||||
|
lease)
|
||||||
|
api_session.invoke_api.assert_called_once_with(vim_util,
|
||||||
|
'get_object_property',
|
||||||
|
api_session.vim,
|
||||||
|
lease, 'state')
|
||||||
|
|
||||||
|
def test_wait_for_lease_ready_with_invoke_api_exception(self):
|
||||||
|
api_session = self._create_api_session(True)
|
||||||
|
api_session.invoke_api = mock.Mock(
|
||||||
|
side_effect=exceptions.VimException(None))
|
||||||
|
lease = mock.Mock()
|
||||||
|
self.assertRaises(exceptions.VimException,
|
||||||
|
api_session.wait_for_lease_ready,
|
||||||
|
lease)
|
||||||
|
api_session.invoke_api.assert_called_once_with(
|
||||||
|
vim_util, 'get_object_property', api_session.vim, lease,
|
||||||
|
'state')
|
||||||
|
|
||||||
|
def _poll_task_well_known_exceptions(self, fault,
|
||||||
|
expected_exception):
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
|
||||||
|
def fake_invoke_api(self, module, method, *args, **kwargs):
|
||||||
|
task_info = mock.Mock()
|
||||||
|
task_info.progress = -1
|
||||||
|
task_info.state = 'error'
|
||||||
|
error = mock.Mock()
|
||||||
|
error.localizedMessage = "Error message"
|
||||||
|
error_fault = mock.Mock()
|
||||||
|
error_fault.__class__.__name__ = fault
|
||||||
|
error.fault = error_fault
|
||||||
|
task_info.error = error
|
||||||
|
return task_info
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(api_session, 'invoke_api', fake_invoke_api)
|
||||||
|
):
|
||||||
|
self.assertRaises(expected_exception,
|
||||||
|
api_session._poll_task,
|
||||||
|
'fake-task')
|
||||||
|
|
||||||
|
def test_poll_task_well_known_exceptions(self):
|
||||||
|
for k, v in six.iteritems(exceptions._fault_classes_registry):
|
||||||
|
self._poll_task_well_known_exceptions(k, v)
|
||||||
|
|
||||||
|
def test_poll_task_unknown_exception(self):
|
||||||
|
_unknown_exceptions = {
|
||||||
|
'NoDiskSpace': exceptions.VMwareDriverException,
|
||||||
|
'RuntimeFault': exceptions.VMwareDriverException
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in six.iteritems(_unknown_exceptions):
|
||||||
|
self._poll_task_well_known_exceptions(k, v)
|
||||||
|
|
||||||
|
def _create_subclass_exception(self):
|
||||||
|
class VimSubClass(exceptions.VMwareDriverException):
|
||||||
|
pass
|
||||||
|
return VimSubClass
|
||||||
|
|
||||||
|
def test_register_fault_class(self):
|
||||||
|
exc = self._create_subclass_exception()
|
||||||
|
exceptions.register_fault_class('ValueError', exc)
|
||||||
|
self.assertEqual(exc, exceptions.get_fault_class('ValueError'))
|
||||||
|
|
||||||
|
def test_register_fault_class_override(self):
|
||||||
|
exc = self._create_subclass_exception()
|
||||||
|
exceptions.register_fault_class(exceptions.ALREADY_EXISTS, exc)
|
||||||
|
self.assertEqual(exc,
|
||||||
|
exceptions.get_fault_class(exceptions.ALREADY_EXISTS))
|
||||||
|
|
||||||
|
def test_register_fault_classi_invalid(self):
|
||||||
|
self.assertRaises(TypeError,
|
||||||
|
exceptions.register_fault_class,
|
||||||
|
'ValueError', ValueError)
|
||||||
|
|
||||||
|
def test_update_pbm_wsdl_loc(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.key = "12345"
|
||||||
|
api_session = self._create_api_session(False)
|
||||||
|
self.assertIsNone(api_session._pbm_wsdl_loc)
|
||||||
|
api_session.pbm_wsdl_loc_set('fake_wsdl')
|
||||||
|
self.assertEqual('fake_wsdl', api_session._pbm_wsdl_loc)
|
552
oslo_vmware/tests/test_image_transfer.py
Normal file
552
oslo_vmware/tests/test_image_transfer.py
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
# 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 functions and classes for image transfer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from eventlet import greenthread
|
||||||
|
from eventlet import timeout
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import image_transfer
|
||||||
|
from oslo_vmware import rw_handles
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class BlockingQueueTest(base.TestCase):
|
||||||
|
"""Tests for BlockingQueue."""
|
||||||
|
|
||||||
|
def test_read(self):
|
||||||
|
max_size = 10
|
||||||
|
chunk_size = 10
|
||||||
|
max_transfer_size = 30
|
||||||
|
queue = image_transfer.BlockingQueue(max_size, max_transfer_size)
|
||||||
|
|
||||||
|
def get_side_effect():
|
||||||
|
return [1] * chunk_size
|
||||||
|
|
||||||
|
queue.get = mock.Mock(side_effect=get_side_effect)
|
||||||
|
while True:
|
||||||
|
data_item = queue.read(chunk_size)
|
||||||
|
if not data_item:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertEqual(max_transfer_size, queue._transferred)
|
||||||
|
exp_calls = [mock.call()] * int(math.ceil(float(max_transfer_size) /
|
||||||
|
chunk_size))
|
||||||
|
self.assertEqual(exp_calls, queue.get.call_args_list)
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
queue = image_transfer.BlockingQueue(10, 30)
|
||||||
|
queue.put = mock.Mock()
|
||||||
|
write_count = 10
|
||||||
|
for _ in range(0, write_count):
|
||||||
|
queue.write([1])
|
||||||
|
exp_calls = [mock.call([1])] * write_count
|
||||||
|
self.assertEqual(exp_calls, queue.put.call_args_list)
|
||||||
|
|
||||||
|
def test_seek(self):
|
||||||
|
queue = image_transfer.BlockingQueue(10, 30)
|
||||||
|
self.assertRaises(IOError, queue.seek, 5)
|
||||||
|
|
||||||
|
def test_tell(self):
|
||||||
|
queue = image_transfer.BlockingQueue(10, 30)
|
||||||
|
self.assertEqual(0, queue.tell())
|
||||||
|
queue.get = mock.Mock(return_value=[1] * 10)
|
||||||
|
queue.read(10)
|
||||||
|
self.assertEqual(10, queue.tell())
|
||||||
|
|
||||||
|
|
||||||
|
class ImageWriterTest(base.TestCase):
|
||||||
|
"""Tests for ImageWriter class."""
|
||||||
|
|
||||||
|
def _create_image_writer(self):
|
||||||
|
self.image_service = mock.Mock()
|
||||||
|
self.context = mock.Mock()
|
||||||
|
self.input_file = mock.Mock()
|
||||||
|
self.image_id = mock.Mock()
|
||||||
|
return image_transfer.ImageWriter(self.context, self.input_file,
|
||||||
|
self.image_service, self.image_id)
|
||||||
|
|
||||||
|
@mock.patch.object(greenthread, 'sleep')
|
||||||
|
def test_start(self, mock_sleep):
|
||||||
|
writer = self._create_image_writer()
|
||||||
|
status_list = ['queued', 'saving', 'active']
|
||||||
|
|
||||||
|
def image_service_show_side_effect(context, image_id):
|
||||||
|
status = status_list.pop(0)
|
||||||
|
return {'status': status}
|
||||||
|
|
||||||
|
self.image_service.show.side_effect = image_service_show_side_effect
|
||||||
|
exp_calls = [mock.call(self.context, self.image_id)] * len(status_list)
|
||||||
|
writer.start()
|
||||||
|
self.assertTrue(writer.wait())
|
||||||
|
self.image_service.update.assert_called_once_with(self.context,
|
||||||
|
self.image_id, {},
|
||||||
|
data=self.input_file)
|
||||||
|
self.assertEqual(exp_calls, self.image_service.show.call_args_list)
|
||||||
|
|
||||||
|
def test_start_with_killed_status(self):
|
||||||
|
writer = self._create_image_writer()
|
||||||
|
|
||||||
|
def image_service_show_side_effect(_context, _image_id):
|
||||||
|
return {'status': 'killed'}
|
||||||
|
|
||||||
|
self.image_service.show.side_effect = image_service_show_side_effect
|
||||||
|
writer.start()
|
||||||
|
self.assertRaises(exceptions.ImageTransferException,
|
||||||
|
writer.wait)
|
||||||
|
self.image_service.update.assert_called_once_with(self.context,
|
||||||
|
self.image_id, {},
|
||||||
|
data=self.input_file)
|
||||||
|
self.image_service.show.assert_called_once_with(self.context,
|
||||||
|
self.image_id)
|
||||||
|
|
||||||
|
def test_start_with_unknown_status(self):
|
||||||
|
writer = self._create_image_writer()
|
||||||
|
|
||||||
|
def image_service_show_side_effect(_context, _image_id):
|
||||||
|
return {'status': 'unknown'}
|
||||||
|
|
||||||
|
self.image_service.show.side_effect = image_service_show_side_effect
|
||||||
|
writer.start()
|
||||||
|
self.assertRaises(exceptions.ImageTransferException,
|
||||||
|
writer.wait)
|
||||||
|
self.image_service.update.assert_called_once_with(self.context,
|
||||||
|
self.image_id, {},
|
||||||
|
data=self.input_file)
|
||||||
|
self.image_service.show.assert_called_once_with(self.context,
|
||||||
|
self.image_id)
|
||||||
|
|
||||||
|
def test_start_with_image_service_show_exception(self):
|
||||||
|
writer = self._create_image_writer()
|
||||||
|
self.image_service.show.side_effect = RuntimeError()
|
||||||
|
writer.start()
|
||||||
|
self.assertRaises(exceptions.ImageTransferException, writer.wait)
|
||||||
|
self.image_service.update.assert_called_once_with(self.context,
|
||||||
|
self.image_id, {},
|
||||||
|
data=self.input_file)
|
||||||
|
self.image_service.show.assert_called_once_with(self.context,
|
||||||
|
self.image_id)
|
||||||
|
|
||||||
|
|
||||||
|
class FileReadWriteTaskTest(base.TestCase):
|
||||||
|
"""Tests for FileReadWriteTask class."""
|
||||||
|
|
||||||
|
def test_start(self):
|
||||||
|
data_items = [[1] * 10, [1] * 20, [1] * 5, []]
|
||||||
|
|
||||||
|
def input_file_read_side_effect(arg):
|
||||||
|
self.assertEqual(arg, rw_handles.READ_CHUNKSIZE)
|
||||||
|
data = data_items[input_file_read_side_effect.i]
|
||||||
|
input_file_read_side_effect.i += 1
|
||||||
|
return data
|
||||||
|
|
||||||
|
input_file_read_side_effect.i = 0
|
||||||
|
input_file = mock.Mock()
|
||||||
|
input_file.read.side_effect = input_file_read_side_effect
|
||||||
|
output_file = mock.Mock()
|
||||||
|
rw_task = image_transfer.FileReadWriteTask(input_file, output_file)
|
||||||
|
rw_task.start()
|
||||||
|
self.assertTrue(rw_task.wait())
|
||||||
|
self.assertEqual(len(data_items), input_file.read.call_count)
|
||||||
|
|
||||||
|
exp_calls = []
|
||||||
|
for i in range(0, len(data_items)):
|
||||||
|
exp_calls.append(mock.call(data_items[i]))
|
||||||
|
self.assertEqual(exp_calls, output_file.write.call_args_list)
|
||||||
|
|
||||||
|
self.assertEqual(len(data_items),
|
||||||
|
input_file.update_progress.call_count)
|
||||||
|
self.assertEqual(len(data_items),
|
||||||
|
output_file.update_progress.call_count)
|
||||||
|
|
||||||
|
def test_start_with_read_exception(self):
|
||||||
|
input_file = mock.Mock()
|
||||||
|
input_file.read.side_effect = RuntimeError()
|
||||||
|
output_file = mock.Mock()
|
||||||
|
rw_task = image_transfer.FileReadWriteTask(input_file, output_file)
|
||||||
|
rw_task.start()
|
||||||
|
self.assertRaises(exceptions.ImageTransferException, rw_task.wait)
|
||||||
|
input_file.read.assert_called_once_with(rw_handles.READ_CHUNKSIZE)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageTransferUtilityTest(base.TestCase):
|
||||||
|
"""Tests for image_transfer utility methods."""
|
||||||
|
|
||||||
|
@mock.patch.object(timeout, 'Timeout')
|
||||||
|
@mock.patch.object(image_transfer, 'ImageWriter')
|
||||||
|
@mock.patch.object(image_transfer, 'FileReadWriteTask')
|
||||||
|
@mock.patch.object(image_transfer, 'BlockingQueue')
|
||||||
|
def test_start_transfer(self, fake_BlockingQueue, fake_FileReadWriteTask,
|
||||||
|
fake_ImageWriter, fake_Timeout):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
read_file_handle = mock.Mock()
|
||||||
|
read_file_handle.close = mock.Mock()
|
||||||
|
image_service = mock.Mock()
|
||||||
|
image_id = mock.Mock()
|
||||||
|
blocking_queue = mock.Mock()
|
||||||
|
|
||||||
|
write_file_handle1 = mock.Mock()
|
||||||
|
write_file_handle1.close = mock.Mock()
|
||||||
|
write_file_handle2 = None
|
||||||
|
write_file_handles = [write_file_handle1, write_file_handle2]
|
||||||
|
|
||||||
|
timeout_secs = 10
|
||||||
|
blocking_queue_size = 10
|
||||||
|
image_meta = {}
|
||||||
|
max_data_size = 30
|
||||||
|
|
||||||
|
fake_BlockingQueue.return_value = blocking_queue
|
||||||
|
fake_timer = mock.Mock()
|
||||||
|
fake_timer.cancel = mock.Mock()
|
||||||
|
fake_Timeout.return_value = fake_timer
|
||||||
|
|
||||||
|
for write_file_handle in write_file_handles:
|
||||||
|
image_transfer._start_transfer(context,
|
||||||
|
timeout_secs,
|
||||||
|
read_file_handle,
|
||||||
|
max_data_size,
|
||||||
|
write_file_handle=write_file_handle,
|
||||||
|
image_service=image_service,
|
||||||
|
image_id=image_id,
|
||||||
|
image_meta=image_meta)
|
||||||
|
|
||||||
|
exp_calls = [mock.call(blocking_queue_size,
|
||||||
|
max_data_size)] * len(write_file_handles)
|
||||||
|
self.assertEqual(exp_calls,
|
||||||
|
fake_BlockingQueue.call_args_list)
|
||||||
|
|
||||||
|
exp_calls2 = [mock.call(read_file_handle, blocking_queue),
|
||||||
|
mock.call(blocking_queue, write_file_handle1),
|
||||||
|
mock.call(read_file_handle, blocking_queue)]
|
||||||
|
self.assertEqual(exp_calls2,
|
||||||
|
fake_FileReadWriteTask.call_args_list)
|
||||||
|
|
||||||
|
exp_calls3 = mock.call(context, blocking_queue, image_service,
|
||||||
|
image_id, image_meta)
|
||||||
|
self.assertEqual(exp_calls3,
|
||||||
|
fake_ImageWriter.call_args)
|
||||||
|
|
||||||
|
exp_calls4 = [mock.call(timeout_secs)] * len(write_file_handles)
|
||||||
|
self.assertEqual(exp_calls4,
|
||||||
|
fake_Timeout.call_args_list)
|
||||||
|
|
||||||
|
self.assertEqual(len(write_file_handles),
|
||||||
|
fake_timer.cancel.call_count)
|
||||||
|
|
||||||
|
self.assertEqual(len(write_file_handles),
|
||||||
|
read_file_handle.close.call_count)
|
||||||
|
|
||||||
|
write_file_handle1.close.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(image_transfer, 'FileReadWriteTask')
|
||||||
|
@mock.patch.object(image_transfer, 'BlockingQueue')
|
||||||
|
def test_start_transfer_with_no_image_destination(self, fake_BlockingQueue,
|
||||||
|
fake_FileReadWriteTask):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
read_file_handle = mock.Mock()
|
||||||
|
write_file_handle = None
|
||||||
|
image_service = None
|
||||||
|
image_id = None
|
||||||
|
timeout_secs = 10
|
||||||
|
image_meta = {}
|
||||||
|
blocking_queue_size = 10
|
||||||
|
max_data_size = 30
|
||||||
|
blocking_queue = mock.Mock()
|
||||||
|
|
||||||
|
fake_BlockingQueue.return_value = blocking_queue
|
||||||
|
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
image_transfer._start_transfer,
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
read_file_handle,
|
||||||
|
max_data_size,
|
||||||
|
write_file_handle=write_file_handle,
|
||||||
|
image_service=image_service,
|
||||||
|
image_id=image_id,
|
||||||
|
image_meta=image_meta)
|
||||||
|
|
||||||
|
fake_BlockingQueue.assert_called_once_with(blocking_queue_size,
|
||||||
|
max_data_size)
|
||||||
|
|
||||||
|
fake_FileReadWriteTask.assert_called_once_with(read_file_handle,
|
||||||
|
blocking_queue)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.FileWriteHandle')
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.ImageReadHandle')
|
||||||
|
@mock.patch.object(image_transfer, '_start_transfer')
|
||||||
|
def test_download_flat_image(
|
||||||
|
self,
|
||||||
|
fake_transfer,
|
||||||
|
fake_rw_handles_ImageReadHandle,
|
||||||
|
fake_rw_handles_FileWriteHandle):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
image_id = mock.Mock()
|
||||||
|
image_service = mock.Mock()
|
||||||
|
image_service.download = mock.Mock()
|
||||||
|
image_service.download.return_value = 'fake_iter'
|
||||||
|
|
||||||
|
fake_ImageReadHandle = 'fake_ImageReadHandle'
|
||||||
|
fake_FileWriteHandle = 'fake_FileWriteHandle'
|
||||||
|
cookies = []
|
||||||
|
timeout_secs = 10
|
||||||
|
image_size = 1000
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = 443
|
||||||
|
dc_path = 'dc1'
|
||||||
|
ds_name = 'ds1'
|
||||||
|
file_path = '/fake_path'
|
||||||
|
|
||||||
|
fake_rw_handles_ImageReadHandle.return_value = fake_ImageReadHandle
|
||||||
|
fake_rw_handles_FileWriteHandle.return_value = fake_FileWriteHandle
|
||||||
|
|
||||||
|
image_transfer.download_flat_image(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
image_service,
|
||||||
|
image_id,
|
||||||
|
image_size=image_size,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
data_center_name=dc_path,
|
||||||
|
datastore_name=ds_name,
|
||||||
|
cookies=cookies,
|
||||||
|
file_path=file_path)
|
||||||
|
|
||||||
|
image_service.download.assert_called_once_with(context, image_id)
|
||||||
|
|
||||||
|
fake_rw_handles_ImageReadHandle.assert_called_once_with('fake_iter')
|
||||||
|
|
||||||
|
fake_rw_handles_FileWriteHandle.assert_called_once_with(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
dc_path,
|
||||||
|
ds_name,
|
||||||
|
cookies,
|
||||||
|
file_path,
|
||||||
|
image_size,
|
||||||
|
cacerts=None)
|
||||||
|
|
||||||
|
fake_transfer.assert_called_once_with(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
fake_ImageReadHandle,
|
||||||
|
image_size,
|
||||||
|
write_file_handle=fake_FileWriteHandle)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.VmdkWriteHandle')
|
||||||
|
@mock.patch.object(image_transfer, '_start_transfer')
|
||||||
|
def test_download_stream_optimized_data(self, fake_transfer,
|
||||||
|
fake_rw_handles_VmdkWriteHandle):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
session = mock.Mock()
|
||||||
|
read_handle = mock.Mock()
|
||||||
|
timeout_secs = 10
|
||||||
|
image_size = 1000
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = 443
|
||||||
|
resource_pool = 'rp-1'
|
||||||
|
vm_folder = 'folder-1'
|
||||||
|
vm_import_spec = None
|
||||||
|
|
||||||
|
fake_VmdkWriteHandle = mock.Mock()
|
||||||
|
fake_VmdkWriteHandle.get_imported_vm = mock.Mock()
|
||||||
|
fake_rw_handles_VmdkWriteHandle.return_value = fake_VmdkWriteHandle
|
||||||
|
|
||||||
|
image_transfer.download_stream_optimized_data(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
read_handle,
|
||||||
|
session=session,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
resource_pool=resource_pool,
|
||||||
|
vm_folder=vm_folder,
|
||||||
|
vm_import_spec=vm_import_spec,
|
||||||
|
image_size=image_size)
|
||||||
|
|
||||||
|
fake_rw_handles_VmdkWriteHandle.assert_called_once_with(
|
||||||
|
session,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
resource_pool,
|
||||||
|
vm_folder,
|
||||||
|
vm_import_spec,
|
||||||
|
image_size)
|
||||||
|
|
||||||
|
fake_transfer.assert_called_once_with(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
read_handle,
|
||||||
|
image_size,
|
||||||
|
write_file_handle=fake_VmdkWriteHandle)
|
||||||
|
|
||||||
|
fake_VmdkWriteHandle.get_imported_vm.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.ImageReadHandle')
|
||||||
|
@mock.patch.object(image_transfer, 'download_stream_optimized_data')
|
||||||
|
def test_download_stream_optimized_image(
|
||||||
|
self, fake_download_stream_optimized_data,
|
||||||
|
fake_rw_handles_ImageReadHandle):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
session = mock.Mock()
|
||||||
|
image_id = mock.Mock()
|
||||||
|
timeout_secs = 10
|
||||||
|
image_size = 1000
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = 443
|
||||||
|
resource_pool = 'rp-1'
|
||||||
|
vm_folder = 'folder-1'
|
||||||
|
vm_import_spec = None
|
||||||
|
|
||||||
|
fake_iter = 'fake_iter'
|
||||||
|
image_service = mock.Mock()
|
||||||
|
image_service.download = mock.Mock()
|
||||||
|
image_service.download.return_value = fake_iter
|
||||||
|
|
||||||
|
fake_ImageReadHandle = 'fake_ImageReadHandle'
|
||||||
|
fake_rw_handles_ImageReadHandle.return_value = fake_ImageReadHandle
|
||||||
|
|
||||||
|
image_transfer.download_stream_optimized_image(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
image_service,
|
||||||
|
image_id,
|
||||||
|
session=session,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
resource_pool=resource_pool,
|
||||||
|
vm_folder=vm_folder,
|
||||||
|
vm_import_spec=vm_import_spec,
|
||||||
|
image_size=image_size)
|
||||||
|
|
||||||
|
image_service.download.assert_called_once_with(context, image_id)
|
||||||
|
|
||||||
|
fake_rw_handles_ImageReadHandle.assert_called_once_with(fake_iter)
|
||||||
|
|
||||||
|
fake_download_stream_optimized_data.assert_called_once_with(
|
||||||
|
context,
|
||||||
|
timeout_secs,
|
||||||
|
fake_ImageReadHandle,
|
||||||
|
session=session,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
resource_pool=resource_pool,
|
||||||
|
vm_folder=vm_folder,
|
||||||
|
vm_import_spec=vm_import_spec,
|
||||||
|
image_size=image_size)
|
||||||
|
|
||||||
|
@mock.patch.object(image_transfer, '_start_transfer')
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.VmdkReadHandle')
|
||||||
|
def test_copy_stream_optimized_disk(
|
||||||
|
self, vmdk_read_handle, start_transfer):
|
||||||
|
|
||||||
|
read_handle = mock.sentinel.read_handle
|
||||||
|
vmdk_read_handle.return_value = read_handle
|
||||||
|
|
||||||
|
context = mock.sentinel.context
|
||||||
|
timeout = mock.sentinel.timeout
|
||||||
|
write_handle = mock.Mock(name='/cinder/images/tmpAbcd.vmdk')
|
||||||
|
session = mock.sentinel.session
|
||||||
|
host = mock.sentinel.host
|
||||||
|
port = mock.sentinel.port
|
||||||
|
vm = mock.sentinel.vm
|
||||||
|
vmdk_file_path = mock.sentinel.vmdk_file_path
|
||||||
|
vmdk_size = mock.sentinel.vmdk_size
|
||||||
|
|
||||||
|
image_transfer.copy_stream_optimized_disk(
|
||||||
|
context, timeout, write_handle, session=session, host=host,
|
||||||
|
port=port, vm=vm, vmdk_file_path=vmdk_file_path,
|
||||||
|
vmdk_size=vmdk_size)
|
||||||
|
|
||||||
|
vmdk_read_handle.assert_called_once_with(
|
||||||
|
session, host, port, vm, vmdk_file_path, vmdk_size)
|
||||||
|
start_transfer.assert_called_once_with(
|
||||||
|
context, timeout, read_handle, vmdk_size,
|
||||||
|
write_file_handle=write_handle)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.rw_handles.VmdkReadHandle')
|
||||||
|
@mock.patch.object(image_transfer, '_start_transfer')
|
||||||
|
def test_upload_image(self, fake_transfer, fake_rw_handles_VmdkReadHandle):
|
||||||
|
|
||||||
|
context = mock.Mock()
|
||||||
|
image_id = mock.Mock()
|
||||||
|
owner_id = mock.Mock()
|
||||||
|
session = mock.Mock()
|
||||||
|
vm = mock.Mock()
|
||||||
|
image_service = mock.Mock()
|
||||||
|
|
||||||
|
timeout_secs = 10
|
||||||
|
image_size = 1000
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = 443
|
||||||
|
file_path = '/fake_path'
|
||||||
|
is_public = False
|
||||||
|
image_name = 'fake_image'
|
||||||
|
image_version = 1
|
||||||
|
|
||||||
|
fake_VmdkReadHandle = 'fake_VmdkReadHandle'
|
||||||
|
fake_rw_handles_VmdkReadHandle.return_value = fake_VmdkReadHandle
|
||||||
|
|
||||||
|
image_transfer.upload_image(context,
|
||||||
|
timeout_secs,
|
||||||
|
image_service,
|
||||||
|
image_id,
|
||||||
|
owner_id,
|
||||||
|
session=session,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
vm=vm,
|
||||||
|
vmdk_file_path=file_path,
|
||||||
|
vmdk_size=image_size,
|
||||||
|
is_public=is_public,
|
||||||
|
image_name=image_name,
|
||||||
|
image_version=image_version)
|
||||||
|
|
||||||
|
fake_rw_handles_VmdkReadHandle.assert_called_once_with(session,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
vm,
|
||||||
|
file_path,
|
||||||
|
image_size)
|
||||||
|
|
||||||
|
image_metadata = {'disk_format': 'vmdk',
|
||||||
|
'is_public': is_public,
|
||||||
|
'name': image_name,
|
||||||
|
'status': 'active',
|
||||||
|
'container_format': 'bare',
|
||||||
|
'size': 0,
|
||||||
|
'properties': {'vmware_image_version': image_version,
|
||||||
|
'vmware_disktype': 'streamOptimized',
|
||||||
|
'owner_id': owner_id}}
|
||||||
|
|
||||||
|
fake_transfer.assert_called_once_with(context,
|
||||||
|
timeout_secs,
|
||||||
|
fake_VmdkReadHandle,
|
||||||
|
0,
|
||||||
|
image_service=image_service,
|
||||||
|
image_id=image_id,
|
||||||
|
image_meta=image_metadata)
|
173
oslo_vmware/tests/test_pbm.py
Normal file
173
oslo_vmware/tests/test_pbm.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# 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 PBM utility methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
import six.moves.urllib.request as urllib
|
||||||
|
|
||||||
|
from oslo_vmware import pbm
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class PBMUtilityTest(base.TestCase):
|
||||||
|
"""Tests for PBM utility methods."""
|
||||||
|
|
||||||
|
def test_get_all_profiles(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.pbm = mock.Mock()
|
||||||
|
profile_ids = mock.Mock()
|
||||||
|
|
||||||
|
def invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
|
self.assertEqual(session.pbm, module)
|
||||||
|
self.assertTrue(method in ['PbmQueryProfile',
|
||||||
|
'PbmRetrieveContent'])
|
||||||
|
self.assertEqual(session.pbm.service_content.profileManager,
|
||||||
|
args[0])
|
||||||
|
if method == 'PbmQueryProfile':
|
||||||
|
self.assertEqual('STORAGE',
|
||||||
|
kwargs['resourceType'].resourceType)
|
||||||
|
return profile_ids
|
||||||
|
self.assertEqual(profile_ids, kwargs['profileIds'])
|
||||||
|
|
||||||
|
session.invoke_api.side_effect = invoke_api_side_effect
|
||||||
|
pbm.get_all_profiles(session)
|
||||||
|
self.assertEqual(2, session.invoke_api.call_count)
|
||||||
|
|
||||||
|
def test_get_all_profiles_with_no_profiles(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
session.pbm = mock.Mock()
|
||||||
|
session.invoke_api.return_value = []
|
||||||
|
profiles = pbm.get_all_profiles(session)
|
||||||
|
session.invoke_api.assert_called_once_with(
|
||||||
|
session.pbm,
|
||||||
|
'PbmQueryProfile',
|
||||||
|
session.pbm.service_content.profileManager,
|
||||||
|
resourceType=session.pbm.client.factory.create())
|
||||||
|
self.assertEqual([], profiles)
|
||||||
|
|
||||||
|
def _create_profile(self, profile_id, name):
|
||||||
|
profile = mock.Mock()
|
||||||
|
profile.profileId = profile_id
|
||||||
|
profile.name = name
|
||||||
|
return profile
|
||||||
|
|
||||||
|
@mock.patch.object(pbm, 'get_all_profiles')
|
||||||
|
def test_get_profile_id_by_name(self, get_all_profiles):
|
||||||
|
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
||||||
|
for i in range(0, 10)]
|
||||||
|
get_all_profiles.return_value = profiles
|
||||||
|
|
||||||
|
session = mock.Mock()
|
||||||
|
exp_profile_id = '5'
|
||||||
|
profile_id = pbm.get_profile_id_by_name(session,
|
||||||
|
'profile-%s' % exp_profile_id)
|
||||||
|
self.assertEqual(exp_profile_id, profile_id)
|
||||||
|
get_all_profiles.assert_called_once_with(session)
|
||||||
|
|
||||||
|
@mock.patch.object(pbm, 'get_all_profiles')
|
||||||
|
def test_get_profile_id_by_name_with_invalid_profile(self,
|
||||||
|
get_all_profiles):
|
||||||
|
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
||||||
|
for i in range(0, 10)]
|
||||||
|
get_all_profiles.return_value = profiles
|
||||||
|
|
||||||
|
session = mock.Mock()
|
||||||
|
profile_id = pbm.get_profile_id_by_name(session,
|
||||||
|
('profile-%s' % 11))
|
||||||
|
self.assertFalse(profile_id)
|
||||||
|
get_all_profiles.assert_called_once_with(session)
|
||||||
|
|
||||||
|
def test_filter_hubs_by_profile(self):
|
||||||
|
pbm_client = mock.Mock()
|
||||||
|
session = mock.Mock()
|
||||||
|
session.pbm = pbm_client
|
||||||
|
hubs = mock.Mock()
|
||||||
|
profile_id = 'profile-0'
|
||||||
|
|
||||||
|
pbm.filter_hubs_by_profile(session, hubs, profile_id)
|
||||||
|
session.invoke_api.assert_called_once_with(
|
||||||
|
pbm_client,
|
||||||
|
'PbmQueryMatchingHub',
|
||||||
|
pbm_client.service_content.placementSolver,
|
||||||
|
hubsToSearch=hubs,
|
||||||
|
profile=profile_id)
|
||||||
|
|
||||||
|
def _create_datastore(self, value):
|
||||||
|
ds = mock.Mock()
|
||||||
|
ds.value = value
|
||||||
|
return ds
|
||||||
|
|
||||||
|
def test_convert_datastores_to_hubs(self):
|
||||||
|
ds_values = []
|
||||||
|
datastores = []
|
||||||
|
for i in range(0, 10):
|
||||||
|
value = "ds-%d" % i
|
||||||
|
ds_values.append(value)
|
||||||
|
datastores.append(self._create_datastore(value))
|
||||||
|
|
||||||
|
pbm_client_factory = mock.Mock()
|
||||||
|
pbm_client_factory.create.side_effect = lambda *args: mock.Mock()
|
||||||
|
hubs = pbm.convert_datastores_to_hubs(pbm_client_factory, datastores)
|
||||||
|
self.assertEqual(len(datastores), len(hubs))
|
||||||
|
hub_ids = [hub.hubId for hub in hubs]
|
||||||
|
self.assertEqual(set(ds_values), set(hub_ids))
|
||||||
|
|
||||||
|
def test_filter_datastores_by_hubs(self):
|
||||||
|
ds_values = []
|
||||||
|
datastores = []
|
||||||
|
for i in range(0, 10):
|
||||||
|
value = "ds-%d" % i
|
||||||
|
ds_values.append(value)
|
||||||
|
datastores.append(self._create_datastore(value))
|
||||||
|
|
||||||
|
hubs = []
|
||||||
|
hub_ids = ds_values[0:int(len(ds_values) / 2)]
|
||||||
|
for hub_id in hub_ids:
|
||||||
|
hub = mock.Mock()
|
||||||
|
hub.hubId = hub_id
|
||||||
|
hubs.append(hub)
|
||||||
|
|
||||||
|
filtered_ds = pbm.filter_datastores_by_hubs(hubs, datastores)
|
||||||
|
self.assertEqual(len(hubs), len(filtered_ds))
|
||||||
|
filtered_ds_values = [ds.value for ds in filtered_ds]
|
||||||
|
self.assertEqual(set(hub_ids), set(filtered_ds_values))
|
||||||
|
|
||||||
|
def test_get_pbm_wsdl_location(self):
|
||||||
|
wsdl = pbm.get_pbm_wsdl_location(None)
|
||||||
|
self.assertIsNone(wsdl)
|
||||||
|
|
||||||
|
def expected_wsdl(version):
|
||||||
|
driver_abs_dir = os.path.abspath(os.path.dirname(pbm.__file__))
|
||||||
|
path = os.path.join(driver_abs_dir, 'wsdl', version,
|
||||||
|
'pbmService.wsdl')
|
||||||
|
return urlparse.urljoin('file:', urllib.pathname2url(path))
|
||||||
|
|
||||||
|
with mock.patch('os.path.exists') as path_exists:
|
||||||
|
path_exists.return_value = True
|
||||||
|
wsdl = pbm.get_pbm_wsdl_location('5')
|
||||||
|
self.assertEqual(expected_wsdl('5'), wsdl)
|
||||||
|
wsdl = pbm.get_pbm_wsdl_location('5.5')
|
||||||
|
self.assertEqual(expected_wsdl('5.5'), wsdl)
|
||||||
|
wsdl = pbm.get_pbm_wsdl_location('5.5.1')
|
||||||
|
self.assertEqual(expected_wsdl('5.5'), wsdl)
|
||||||
|
path_exists.return_value = False
|
||||||
|
wsdl = pbm.get_pbm_wsdl_location('5.5')
|
||||||
|
self.assertIsNone(wsdl)
|
302
oslo_vmware/tests/test_rw_handles.py
Normal file
302
oslo_vmware/tests/test_rw_handles.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# 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
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import rw_handles
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
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_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'
|
||||||
|
port = 443
|
||||||
|
exp_url = 'https://%s:%d/ds1/vm1.vmdk' % (host, port)
|
||||||
|
vmw_http_file = rw_handles.FileHandle(None)
|
||||||
|
self.assertEqual(exp_url, vmw_http_file._find_vmdk_url(lease_info,
|
||||||
|
host,
|
||||||
|
port))
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
'urllib3.connection.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', 443, '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(
|
||||||
|
'urllib3.connection.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,
|
||||||
|
rw_handles.VmdkWriteHandle,
|
||||||
|
session,
|
||||||
|
'10.1.2.3',
|
||||||
|
443,
|
||||||
|
'rp-1',
|
||||||
|
'folder-1',
|
||||||
|
None,
|
||||||
|
100)
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
session = self._create_mock_session()
|
||||||
|
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3', 443,
|
||||||
|
'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', 443,
|
||||||
|
'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', 443,
|
||||||
|
'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', 443,
|
||||||
|
'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()
|
||||||
|
|
||||||
|
send_patcher = mock.patch('requests.sessions.Session.send')
|
||||||
|
self.addCleanup(send_patcher.stop)
|
||||||
|
send_mock = send_patcher.start()
|
||||||
|
self._response = mock.Mock()
|
||||||
|
send_mock.return_value = self._response
|
||||||
|
|
||||||
|
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,
|
||||||
|
rw_handles.VmdkReadHandle,
|
||||||
|
session,
|
||||||
|
'10.1.2.3',
|
||||||
|
443,
|
||||||
|
'vm-1',
|
||||||
|
'[ds] disk1.vmdk',
|
||||||
|
100)
|
||||||
|
|
||||||
|
def test_read(self):
|
||||||
|
chunk_size = rw_handles.READ_CHUNKSIZE
|
||||||
|
session = self._create_mock_session()
|
||||||
|
self._response.raw.read.return_value = [1] * chunk_size
|
||||||
|
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
|
||||||
|
'vm-1', '[ds] disk1.vmdk',
|
||||||
|
chunk_size * 10)
|
||||||
|
handle.read(chunk_size)
|
||||||
|
self.assertEqual(chunk_size, handle._bytes_read)
|
||||||
|
self._response.raw.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._response.raw.read.return_value = [1] * chunk_size
|
||||||
|
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
|
||||||
|
'vm-1', '[ds] disk1.vmdk',
|
||||||
|
vmdk_size)
|
||||||
|
handle.read(chunk_size)
|
||||||
|
handle.update_progress()
|
||||||
|
self._response.raw.read.assert_called_once_with(chunk_size)
|
||||||
|
|
||||||
|
def test_update_progress_with_error(self):
|
||||||
|
session = self._create_mock_session(True, 10)
|
||||||
|
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
|
||||||
|
'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', 443,
|
||||||
|
'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(six.Iterator):
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
next = __next__
|
||||||
|
|
||||||
|
handle = rw_handles.ImageReadHandle(ImageReadIterator())
|
||||||
|
for _ in range(0, max_items):
|
||||||
|
self.assertEqual(item, handle.read(10))
|
||||||
|
self.assertFalse(handle.read(10))
|
446
oslo_vmware/tests/test_service.py
Normal file
446
oslo_vmware/tests/test_service.py
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
import six.moves.http_client as httplib
|
||||||
|
import suds
|
||||||
|
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware import service
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceMessagePluginTest(base.TestCase):
|
||||||
|
"""Test class for ServiceMessagePlugin."""
|
||||||
|
|
||||||
|
def test_add_attribute_for_value(self):
|
||||||
|
node = mock.Mock()
|
||||||
|
node.name = 'value'
|
||||||
|
plugin = service.ServiceMessagePlugin()
|
||||||
|
plugin.add_attribute_for_value(node)
|
||||||
|
node.set.assert_called_once_with('xsi:type', 'xsd:string')
|
||||||
|
|
||||||
|
def test_marshalled(self):
|
||||||
|
plugin = service.ServiceMessagePlugin()
|
||||||
|
context = mock.Mock()
|
||||||
|
plugin.marshalled(context)
|
||||||
|
context.envelope.prune.assert_called_once_with()
|
||||||
|
context.envelope.walk.assert_called_once_with(
|
||||||
|
plugin.add_attribute_for_value)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ServiceTest, self).setUp()
|
||||||
|
patcher = mock.patch('suds.client.Client')
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.SudsClientMock = patcher.start()
|
||||||
|
|
||||||
|
def test_retrieve_properties_ex_fault_checker_with_empty_response(self):
|
||||||
|
try:
|
||||||
|
service.Service._retrieve_properties_ex_fault_checker(None)
|
||||||
|
assert False
|
||||||
|
except exceptions.VimFaultException as ex:
|
||||||
|
self.assertEqual([exceptions.NOT_AUTHENTICATED],
|
||||||
|
ex.fault_list)
|
||||||
|
|
||||||
|
def test_retrieve_properties_ex_fault_checker(self):
|
||||||
|
fault_list = ['FileFault', 'VimFault']
|
||||||
|
missing_set = []
|
||||||
|
for fault in fault_list:
|
||||||
|
missing_elem = mock.Mock()
|
||||||
|
missing_elem.fault.fault.__class__.__name__ = fault
|
||||||
|
missing_set.append(missing_elem)
|
||||||
|
obj_cont = mock.Mock()
|
||||||
|
obj_cont.missingSet = missing_set
|
||||||
|
response = mock.Mock()
|
||||||
|
response.objects = [obj_cont]
|
||||||
|
|
||||||
|
try:
|
||||||
|
service.Service._retrieve_properties_ex_fault_checker(response)
|
||||||
|
assert False
|
||||||
|
except exceptions.VimFaultException as ex:
|
||||||
|
self.assertEqual(fault_list, ex.fault_list)
|
||||||
|
|
||||||
|
def test_request_handler(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
resp = mock.Mock()
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
ret = svc_obj.powerOn(managed_object)
|
||||||
|
self.assertEqual(resp, ret)
|
||||||
|
|
||||||
|
def test_request_handler_with_retrieve_properties_ex_fault(self):
|
||||||
|
managed_object = 'Datacenter'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'retrievePropertiesEx'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimFaultException,
|
||||||
|
svc_obj.retrievePropertiesEx,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_web_fault(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
fault_list = ['Fault']
|
||||||
|
|
||||||
|
doc = mock.Mock()
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
fault = mock.Mock(faultstring="MyFault")
|
||||||
|
|
||||||
|
fault_children = mock.Mock()
|
||||||
|
fault_children.name = "name"
|
||||||
|
fault_children.getText.return_value = "value"
|
||||||
|
child = mock.Mock()
|
||||||
|
child.get.return_value = fault_list[0]
|
||||||
|
child.getChildren.return_value = [fault_children]
|
||||||
|
detail = mock.Mock()
|
||||||
|
detail.getChildren.return_value = [child]
|
||||||
|
doc.childAtPath.return_value = detail
|
||||||
|
raise suds.WebFault(fault, doc)
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, 'powerOn', side_effect)
|
||||||
|
|
||||||
|
ex = self.assertRaises(exceptions.VimFaultException, svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
self.assertEqual(fault_list, ex.fault_list)
|
||||||
|
self.assertEqual({'name': 'value'}, ex.details)
|
||||||
|
self.assertEqual("MyFault", ex.msg)
|
||||||
|
doc.childAtPath.assertCalledOnceWith('/detail')
|
||||||
|
|
||||||
|
def test_request_handler_with_empty_web_fault_doc(self):
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
fault = mock.Mock(faultstring="MyFault")
|
||||||
|
raise suds.WebFault(fault, None)
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, 'powerOn', side_effect)
|
||||||
|
|
||||||
|
ex = self.assertRaises(exceptions.VimFaultException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
'VirtualMachine')
|
||||||
|
self.assertEqual([], ex.fault_list)
|
||||||
|
self.assertEqual({}, ex.details)
|
||||||
|
self.assertEqual("MyFault", ex.msg)
|
||||||
|
|
||||||
|
def test_request_handler_with_vc51_web_fault(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
fault_list = ['Fault']
|
||||||
|
|
||||||
|
doc = mock.Mock()
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
fault = mock.Mock(faultstring="MyFault")
|
||||||
|
|
||||||
|
fault_children = mock.Mock()
|
||||||
|
fault_children.name = "name"
|
||||||
|
fault_children.getText.return_value = "value"
|
||||||
|
child = mock.Mock()
|
||||||
|
child.get.return_value = fault_list[0]
|
||||||
|
child.getChildren.return_value = [fault_children]
|
||||||
|
detail = mock.Mock()
|
||||||
|
detail.getChildren.return_value = [child]
|
||||||
|
doc.childAtPath.side_effect = [None, detail]
|
||||||
|
raise suds.WebFault(fault, doc)
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, 'powerOn', side_effect)
|
||||||
|
|
||||||
|
ex = self.assertRaises(exceptions.VimFaultException, svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
self.assertEqual(fault_list, ex.fault_list)
|
||||||
|
self.assertEqual({'name': 'value'}, ex.details)
|
||||||
|
self.assertEqual("MyFault", ex.msg)
|
||||||
|
exp_calls = [mock.call('/detail'),
|
||||||
|
mock.call('/Envelope/Body/Fault/detail')]
|
||||||
|
self.assertEqual(exp_calls, doc.childAtPath.call_args_list)
|
||||||
|
|
||||||
|
def test_request_handler_with_attribute_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
svc_obj = service.Service()
|
||||||
|
# no powerOn method in Service
|
||||||
|
service_mock = mock.Mock(spec=service.Service)
|
||||||
|
svc_obj.client.service = service_mock
|
||||||
|
self.assertRaises(exceptions.VimAttributeException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_http_cannot_send_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise httplib.CannotSendRequest()
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimSessionOverLoadException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_http_response_not_ready_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise httplib.ResponseNotReady()
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimSessionOverLoadException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_http_cannot_send_header_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise httplib.CannotSendHeader()
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimSessionOverLoadException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_connection_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise requests.ConnectionError()
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimConnectionException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_http_error(self):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise requests.HTTPError()
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exceptions.VimConnectionException,
|
||||||
|
svc_obj.powerOn,
|
||||||
|
managed_object)
|
||||||
|
|
||||||
|
@mock.patch.object(vim_util, 'get_moref', return_value=None)
|
||||||
|
def test_request_handler_no_value(self, mock_moref):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
svc_obj = service.Service()
|
||||||
|
ret = svc_obj.UnregisterVM(managed_object)
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
|
||||||
|
def _test_request_handler_with_exception(self, message, exception):
|
||||||
|
managed_object = 'VirtualMachine'
|
||||||
|
|
||||||
|
def side_effect(mo, **kwargs):
|
||||||
|
self.assertEqual(managed_object, mo._type)
|
||||||
|
self.assertEqual(managed_object, mo.value)
|
||||||
|
raise Exception(message)
|
||||||
|
|
||||||
|
svc_obj = service.Service()
|
||||||
|
attr_name = 'powerOn'
|
||||||
|
service_mock = svc_obj.client.service
|
||||||
|
setattr(service_mock, attr_name, side_effect)
|
||||||
|
self.assertRaises(exception, svc_obj.powerOn, managed_object)
|
||||||
|
|
||||||
|
def test_request_handler_with_address_in_use_error(self):
|
||||||
|
self._test_request_handler_with_exception(
|
||||||
|
service.ADDRESS_IN_USE_ERROR,
|
||||||
|
exceptions.VimSessionOverLoadException)
|
||||||
|
|
||||||
|
def test_request_handler_with_conn_abort_error(self):
|
||||||
|
self._test_request_handler_with_exception(
|
||||||
|
service.CONN_ABORT_ERROR, exceptions.VimSessionOverLoadException)
|
||||||
|
|
||||||
|
def test_request_handler_with_resp_not_xml_error(self):
|
||||||
|
self._test_request_handler_with_exception(
|
||||||
|
service.RESP_NOT_XML_ERROR, exceptions.VimSessionOverLoadException)
|
||||||
|
|
||||||
|
def test_request_handler_with_generic_error(self):
|
||||||
|
self._test_request_handler_with_exception(
|
||||||
|
'GENERIC_ERROR', exceptions.VimException)
|
||||||
|
|
||||||
|
def test_get_session_cookie(self):
|
||||||
|
svc_obj = service.Service()
|
||||||
|
cookie_value = 'xyz'
|
||||||
|
cookie = mock.Mock()
|
||||||
|
cookie.name = 'vmware_soap_session'
|
||||||
|
cookie.value = cookie_value
|
||||||
|
svc_obj.client.options.transport.cookiejar = [cookie]
|
||||||
|
self.assertEqual(cookie_value, svc_obj.get_http_cookie())
|
||||||
|
|
||||||
|
def test_get_session_cookie_with_no_cookie(self):
|
||||||
|
svc_obj = service.Service()
|
||||||
|
cookie = mock.Mock()
|
||||||
|
cookie.name = 'cookie'
|
||||||
|
cookie.value = 'xyz'
|
||||||
|
svc_obj.client.options.transport.cookiejar = [cookie]
|
||||||
|
self.assertIsNone(svc_obj.get_http_cookie())
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCacheTest(base.TestCase):
|
||||||
|
"""Test class for MemoryCache."""
|
||||||
|
|
||||||
|
def test_get_set(self):
|
||||||
|
cache = service.MemoryCache()
|
||||||
|
cache.put('key1', 'value1')
|
||||||
|
cache.put('key2', 'value2')
|
||||||
|
self.assertEqual('value1', cache.get('key1'))
|
||||||
|
self.assertEqual('value2', cache.get('key2'))
|
||||||
|
self.assertEqual(None, cache.get('key3'))
|
||||||
|
|
||||||
|
@mock.patch('suds.reader.DocumentReader.download')
|
||||||
|
def test_shared_cache(self, mock_reader):
|
||||||
|
cache1 = service.Service().client.options.cache
|
||||||
|
cache2 = service.Service().client.options.cache
|
||||||
|
self.assertIs(cache1, cache2)
|
||||||
|
|
||||||
|
@mock.patch('oslo.utils.timeutils.utcnow_ts')
|
||||||
|
def test_cache_timeout(self, mock_utcnow_ts):
|
||||||
|
mock_utcnow_ts.side_effect = [100, 125, 150, 175, 195, 200, 225]
|
||||||
|
|
||||||
|
cache = service.MemoryCache()
|
||||||
|
cache.put('key1', 'value1', 10)
|
||||||
|
cache.put('key2', 'value2', 75)
|
||||||
|
cache.put('key3', 'value3', 100)
|
||||||
|
|
||||||
|
self.assertIsNone(cache.get('key1'))
|
||||||
|
self.assertEqual('value2', cache.get('key2'))
|
||||||
|
self.assertIsNone(cache.get('key2'))
|
||||||
|
self.assertEqual('value3', cache.get('key3'))
|
||||||
|
|
||||||
|
|
||||||
|
class RequestsTransportTest(base.TestCase):
|
||||||
|
"""Tests for RequestsTransport."""
|
||||||
|
|
||||||
|
def test_open(self):
|
||||||
|
transport = service.RequestsTransport()
|
||||||
|
|
||||||
|
data = "Hello World"
|
||||||
|
resp = mock.Mock(content=data)
|
||||||
|
transport.session.get = mock.Mock(return_value=resp)
|
||||||
|
|
||||||
|
request = mock.Mock(url=mock.sentinel.url)
|
||||||
|
self.assertEqual(data,
|
||||||
|
transport.open(request).getvalue())
|
||||||
|
transport.session.get.assert_called_once_with(mock.sentinel.url,
|
||||||
|
verify=transport.verify)
|
||||||
|
|
||||||
|
def test_send(self):
|
||||||
|
transport = service.RequestsTransport()
|
||||||
|
|
||||||
|
resp = mock.Mock(status_code=mock.sentinel.status_code,
|
||||||
|
headers=mock.sentinel.headers,
|
||||||
|
content=mock.sentinel.content)
|
||||||
|
transport.session.post = mock.Mock(return_value=resp)
|
||||||
|
|
||||||
|
request = mock.Mock(url=mock.sentinel.url,
|
||||||
|
message=mock.sentinel.message,
|
||||||
|
headers=mock.sentinel.req_headers)
|
||||||
|
reply = transport.send(request)
|
||||||
|
|
||||||
|
self.assertEqual(mock.sentinel.status_code, reply.code)
|
||||||
|
self.assertEqual(mock.sentinel.headers, reply.headers)
|
||||||
|
self.assertEqual(mock.sentinel.content, reply.message)
|
||||||
|
|
||||||
|
@mock.patch('os.path.getsize')
|
||||||
|
def test_send_with_local_file_url(self, get_size_mock):
|
||||||
|
transport = service.RequestsTransport()
|
||||||
|
|
||||||
|
url = 'file:///foo'
|
||||||
|
request = requests.PreparedRequest()
|
||||||
|
request.url = url
|
||||||
|
|
||||||
|
data = b"Hello World"
|
||||||
|
get_size_mock.return_value = len(data)
|
||||||
|
|
||||||
|
def readinto_mock(buf):
|
||||||
|
buf[0:] = data
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
builtin_open = 'builtins.open'
|
||||||
|
open_mock = mock.MagicMock(name='file_handle',
|
||||||
|
spec=open)
|
||||||
|
import _io
|
||||||
|
file_spec = list(set(dir(_io.TextIOWrapper)).union(
|
||||||
|
set(dir(_io.BytesIO))))
|
||||||
|
else:
|
||||||
|
builtin_open = '__builtin__.open'
|
||||||
|
open_mock = mock.MagicMock(name='file_handle',
|
||||||
|
spec=file)
|
||||||
|
file_spec = file
|
||||||
|
|
||||||
|
file_handle = mock.MagicMock(spec=file_spec)
|
||||||
|
file_handle.write.return_value = None
|
||||||
|
file_handle.__enter__.return_value = file_handle
|
||||||
|
file_handle.readinto.side_effect = readinto_mock
|
||||||
|
open_mock.return_value = file_handle
|
||||||
|
|
||||||
|
with mock.patch(builtin_open, open_mock, create=True):
|
||||||
|
resp = transport.session.send(request)
|
||||||
|
self.assertEqual(data, resp.content)
|
110
oslo_vmware/tests/test_vim.py
Normal file
110
oslo_vmware/tests/test_vim.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# 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 classes to invoke VMware VI SOAP calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_i18n import fixture as i18n_fixture
|
||||||
|
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
|
from oslo_vmware import exceptions
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim
|
||||||
|
|
||||||
|
|
||||||
|
class VimTest(base.TestCase):
|
||||||
|
"""Test class for Vim."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(VimTest, self).setUp()
|
||||||
|
patcher = mock.patch('suds.client.Client')
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.SudsClientMock = patcher.start()
|
||||||
|
self.useFixture(i18n_fixture.ToggleLazy(True))
|
||||||
|
|
||||||
|
@mock.patch.object(vim.Vim, '__getattr__', autospec=True)
|
||||||
|
def test_service_content(self, getattr_mock):
|
||||||
|
getattr_ret = mock.Mock()
|
||||||
|
getattr_mock.side_effect = lambda *args: getattr_ret
|
||||||
|
vim_obj = vim.Vim()
|
||||||
|
vim_obj.service_content
|
||||||
|
getattr_mock.assert_called_once_with(vim_obj, 'RetrieveServiceContent')
|
||||||
|
getattr_ret.assert_called_once_with('ServiceInstance')
|
||||||
|
self.assertEqual(self.SudsClientMock.return_value, vim_obj.client)
|
||||||
|
self.assertEqual(getattr_ret.return_value, vim_obj.service_content)
|
||||||
|
|
||||||
|
def test_exception_summary_exception_as_list(self):
|
||||||
|
# assert that if a list is fed to the VimException object
|
||||||
|
# that it will error.
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
exceptions.VimException,
|
||||||
|
[], ValueError('foo'))
|
||||||
|
|
||||||
|
def test_exception_summary_string(self):
|
||||||
|
e = exceptions.VimException(_("string"), ValueError("foo"))
|
||||||
|
string = str(e)
|
||||||
|
self.assertEqual("string\nCause: foo", string)
|
||||||
|
|
||||||
|
def test_vim_fault_exception_string(self):
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
exceptions.VimFaultException,
|
||||||
|
"bad", ValueError("argument"))
|
||||||
|
|
||||||
|
def test_vim_fault_exception(self):
|
||||||
|
vfe = exceptions.VimFaultException([ValueError("example")], _("cause"))
|
||||||
|
string = str(vfe)
|
||||||
|
self.assertEqual("cause\nFaults: [ValueError('example',)]", string)
|
||||||
|
|
||||||
|
def test_vim_fault_exception_with_cause_and_details(self):
|
||||||
|
vfe = exceptions.VimFaultException([ValueError("example")],
|
||||||
|
"MyMessage",
|
||||||
|
"FooBar",
|
||||||
|
{'foo': 'bar'})
|
||||||
|
string = str(vfe)
|
||||||
|
self.assertEqual("MyMessage\n"
|
||||||
|
"Cause: FooBar\n"
|
||||||
|
"Faults: [ValueError('example',)]\n"
|
||||||
|
"Details: {'foo': 'bar'}",
|
||||||
|
string)
|
||||||
|
|
||||||
|
def test_configure_non_default_host_port(self):
|
||||||
|
vim_obj = vim.Vim('https', 'www.test.com', 12345)
|
||||||
|
self.assertEqual('https://www.test.com:12345/sdk/vimService.wsdl',
|
||||||
|
vim_obj.wsdl_url)
|
||||||
|
self.assertEqual('https://www.test.com:12345/sdk',
|
||||||
|
vim_obj.soap_url)
|
||||||
|
|
||||||
|
def test_configure_ipv6(self):
|
||||||
|
vim_obj = vim.Vim('https', '::1')
|
||||||
|
self.assertEqual('https://[::1]/sdk/vimService.wsdl',
|
||||||
|
vim_obj.wsdl_url)
|
||||||
|
self.assertEqual('https://[::1]/sdk',
|
||||||
|
vim_obj.soap_url)
|
||||||
|
|
||||||
|
def test_configure_ipv6_and_non_default_host_port(self):
|
||||||
|
vim_obj = vim.Vim('https', '::1', 12345)
|
||||||
|
self.assertEqual('https://[::1]:12345/sdk/vimService.wsdl',
|
||||||
|
vim_obj.wsdl_url)
|
||||||
|
self.assertEqual('https://[::1]:12345/sdk',
|
||||||
|
vim_obj.soap_url)
|
||||||
|
|
||||||
|
def test_configure_with_wsdl_url_override(self):
|
||||||
|
vim_obj = vim.Vim('https', 'www.example.com',
|
||||||
|
wsdl_url='https://test.com/sdk/vimService.wsdl')
|
||||||
|
self.assertEqual('https://test.com/sdk/vimService.wsdl',
|
||||||
|
vim_obj.wsdl_url)
|
||||||
|
self.assertEqual('https://www.example.com/sdk', vim_obj.soap_url)
|
363
oslo_vmware/tests/test_vim_util.py
Normal file
363
oslo_vmware/tests/test_vim_util.py
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# 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 VMware API utility module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_vmware.tests import base
|
||||||
|
from oslo_vmware import vim_util
|
||||||
|
|
||||||
|
|
||||||
|
class VimUtilTest(base.TestCase):
|
||||||
|
"""Test class for utility methods in vim_util."""
|
||||||
|
|
||||||
|
def test_get_moref(self):
|
||||||
|
moref = vim_util.get_moref("vm-0", "VirtualMachine")
|
||||||
|
self.assertEqual("vm-0", moref.value)
|
||||||
|
self.assertEqual("VirtualMachine", moref._type)
|
||||||
|
|
||||||
|
def test_build_selection_spec(self):
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
sel_spec = vim_util.build_selection_spec(client_factory, "test")
|
||||||
|
self.assertEqual("test", sel_spec.name)
|
||||||
|
|
||||||
|
def test_build_traversal_spec(self):
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
sel_spec = mock.Mock()
|
||||||
|
traversal_spec = vim_util.build_traversal_spec(client_factory,
|
||||||
|
'dc_to_hf',
|
||||||
|
'Datacenter',
|
||||||
|
'hostFolder', False,
|
||||||
|
[sel_spec])
|
||||||
|
self.assertEqual("dc_to_hf", traversal_spec.name)
|
||||||
|
self.assertEqual("hostFolder", traversal_spec.path)
|
||||||
|
self.assertEqual([sel_spec], traversal_spec.selectSet)
|
||||||
|
self.assertFalse(traversal_spec.skip)
|
||||||
|
self.assertEqual("Datacenter", traversal_spec.type)
|
||||||
|
|
||||||
|
@mock.patch.object(vim_util, 'build_selection_spec')
|
||||||
|
def test_build_recursive_traversal_spec(self, build_selection_spec_mock):
|
||||||
|
sel_spec = mock.Mock()
|
||||||
|
rp_to_rp_sel_spec = mock.Mock()
|
||||||
|
rp_to_vm_sel_spec = mock.Mock()
|
||||||
|
|
||||||
|
def build_sel_spec_side_effect(client_factory, name):
|
||||||
|
if name == 'visitFolders':
|
||||||
|
return sel_spec
|
||||||
|
elif name == 'rp_to_rp':
|
||||||
|
return rp_to_rp_sel_spec
|
||||||
|
elif name == 'rp_to_vm':
|
||||||
|
return rp_to_vm_sel_spec
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
build_selection_spec_mock.side_effect = build_sel_spec_side_effect
|
||||||
|
traversal_spec_dict = {'dc_to_hf': {'type': 'Datacenter',
|
||||||
|
'path': 'hostFolder',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [sel_spec]},
|
||||||
|
'dc_to_vmf': {'type': 'Datacenter',
|
||||||
|
'path': 'vmFolder',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [sel_spec]},
|
||||||
|
'dc_to_netf': {'type': 'Datacenter',
|
||||||
|
'path': 'networkFolder',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [sel_spec]},
|
||||||
|
'h_to_vm': {'type': 'HostSystem',
|
||||||
|
'path': 'vm',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [sel_spec]},
|
||||||
|
'cr_to_h': {'type': 'ComputeResource',
|
||||||
|
'path': 'host',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': []},
|
||||||
|
'cr_to_ds': {'type': 'ComputeResource',
|
||||||
|
'path': 'datastore',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': []},
|
||||||
|
'cr_to_rp': {'type': 'ComputeResource',
|
||||||
|
'path': 'resourcePool',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [rp_to_rp_sel_spec,
|
||||||
|
rp_to_vm_sel_spec]},
|
||||||
|
'cr_to_rp': {'type': 'ComputeResource',
|
||||||
|
'path': 'resourcePool',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [rp_to_rp_sel_spec,
|
||||||
|
rp_to_vm_sel_spec]},
|
||||||
|
'ccr_to_h': {'type': 'ClusterComputeResource',
|
||||||
|
'path': 'host',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': []},
|
||||||
|
'ccr_to_ds': {'type': 'ClusterComputeResource',
|
||||||
|
'path': 'datastore',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': []},
|
||||||
|
'ccr_to_rp': {'type': 'ClusterComputeResource',
|
||||||
|
'path': 'resourcePool',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [rp_to_rp_sel_spec,
|
||||||
|
rp_to_vm_sel_spec]},
|
||||||
|
'rp_to_rp': {'type': 'ResourcePool',
|
||||||
|
'path': 'resourcePool',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [rp_to_rp_sel_spec,
|
||||||
|
rp_to_vm_sel_spec]},
|
||||||
|
'rp_to_vm': {'type': 'ResourcePool',
|
||||||
|
'path': 'vm',
|
||||||
|
'skip': False,
|
||||||
|
'selectSet': [rp_to_rp_sel_spec,
|
||||||
|
rp_to_vm_sel_spec]},
|
||||||
|
}
|
||||||
|
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
client_factory.create.side_effect = lambda ns: mock.Mock()
|
||||||
|
trav_spec = vim_util.build_recursive_traversal_spec(client_factory)
|
||||||
|
self.assertEqual("visitFolders", trav_spec.name)
|
||||||
|
self.assertEqual("childEntity", trav_spec.path)
|
||||||
|
self.assertFalse(trav_spec.skip)
|
||||||
|
self.assertEqual("Folder", trav_spec.type)
|
||||||
|
|
||||||
|
self.assertEqual(len(traversal_spec_dict) + 1,
|
||||||
|
len(trav_spec.selectSet))
|
||||||
|
for spec in trav_spec.selectSet:
|
||||||
|
if spec.name not in traversal_spec_dict:
|
||||||
|
self.assertEqual(sel_spec, spec)
|
||||||
|
else:
|
||||||
|
exp_spec = traversal_spec_dict[spec.name]
|
||||||
|
self.assertEqual(exp_spec['type'], spec.type)
|
||||||
|
self.assertEqual(exp_spec['path'], spec.path)
|
||||||
|
self.assertEqual(exp_spec['skip'], spec.skip)
|
||||||
|
self.assertEqual(exp_spec['selectSet'], spec.selectSet)
|
||||||
|
|
||||||
|
def test_build_property_spec(self):
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
prop_spec = vim_util.build_property_spec(client_factory)
|
||||||
|
self.assertFalse(prop_spec.all)
|
||||||
|
self.assertEqual(["name"], prop_spec.pathSet)
|
||||||
|
self.assertEqual("VirtualMachine", prop_spec.type)
|
||||||
|
|
||||||
|
def test_build_object_spec(self):
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
root_folder = mock.Mock()
|
||||||
|
specs = [mock.Mock()]
|
||||||
|
obj_spec = vim_util.build_object_spec(client_factory,
|
||||||
|
root_folder, specs)
|
||||||
|
self.assertEqual(root_folder, obj_spec.obj)
|
||||||
|
self.assertEqual(specs, obj_spec.selectSet)
|
||||||
|
self.assertFalse(obj_spec.skip)
|
||||||
|
|
||||||
|
def test_build_property_filter_spec(self):
|
||||||
|
client_factory = mock.Mock()
|
||||||
|
prop_specs = [mock.Mock()]
|
||||||
|
obj_specs = [mock.Mock()]
|
||||||
|
filter_spec = vim_util.build_property_filter_spec(client_factory,
|
||||||
|
prop_specs,
|
||||||
|
obj_specs)
|
||||||
|
self.assertEqual(obj_specs, filter_spec.objectSet)
|
||||||
|
self.assertEqual(prop_specs, filter_spec.propSet)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'oslo_vmware.vim_util.build_recursive_traversal_spec')
|
||||||
|
def test_get_objects(self, build_recursive_traversal_spec):
|
||||||
|
vim = mock.Mock()
|
||||||
|
trav_spec = mock.Mock()
|
||||||
|
build_recursive_traversal_spec.return_value = trav_spec
|
||||||
|
max_objects = 10
|
||||||
|
_type = "VirtualMachine"
|
||||||
|
|
||||||
|
def vim_RetrievePropertiesEx_side_effect(pc, specSet, options):
|
||||||
|
self.assertTrue(pc is vim.service_content.propertyCollector)
|
||||||
|
self.assertEqual(max_objects, options.maxObjects)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(specSet))
|
||||||
|
property_filter_spec = specSet[0]
|
||||||
|
|
||||||
|
propSet = property_filter_spec.propSet
|
||||||
|
self.assertEqual(1, len(propSet))
|
||||||
|
prop_spec = propSet[0]
|
||||||
|
self.assertFalse(prop_spec.all)
|
||||||
|
self.assertEqual(["name"], prop_spec.pathSet)
|
||||||
|
self.assertEqual(_type, prop_spec.type)
|
||||||
|
|
||||||
|
objSet = property_filter_spec.objectSet
|
||||||
|
self.assertEqual(1, len(objSet))
|
||||||
|
obj_spec = objSet[0]
|
||||||
|
self.assertTrue(obj_spec.obj is vim.service_content.rootFolder)
|
||||||
|
self.assertEqual([trav_spec], obj_spec.selectSet)
|
||||||
|
self.assertFalse(obj_spec.skip)
|
||||||
|
|
||||||
|
vim.RetrievePropertiesEx.side_effect = \
|
||||||
|
vim_RetrievePropertiesEx_side_effect
|
||||||
|
vim_util.get_objects(vim, _type, max_objects)
|
||||||
|
self.assertEqual(1, vim.RetrievePropertiesEx.call_count)
|
||||||
|
|
||||||
|
def test_get_object_properties_with_empty_moref(self):
|
||||||
|
vim = mock.Mock()
|
||||||
|
ret = vim_util.get_object_properties(vim, None, None)
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.vim_util.cancel_retrieval')
|
||||||
|
def test_get_object_properties(self, cancel_retrieval):
|
||||||
|
vim = mock.Mock()
|
||||||
|
moref = mock.Mock()
|
||||||
|
moref._type = "VirtualMachine"
|
||||||
|
retrieve_result = mock.Mock()
|
||||||
|
|
||||||
|
def vim_RetrievePropertiesEx_side_effect(pc, specSet, options):
|
||||||
|
self.assertTrue(pc is vim.service_content.propertyCollector)
|
||||||
|
self.assertEqual(1, options.maxObjects)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(specSet))
|
||||||
|
property_filter_spec = specSet[0]
|
||||||
|
|
||||||
|
propSet = property_filter_spec.propSet
|
||||||
|
self.assertEqual(1, len(propSet))
|
||||||
|
prop_spec = propSet[0]
|
||||||
|
self.assertTrue(prop_spec.all)
|
||||||
|
self.assertEqual(['name'], prop_spec.pathSet)
|
||||||
|
self.assertEqual(moref._type, prop_spec.type)
|
||||||
|
|
||||||
|
objSet = property_filter_spec.objectSet
|
||||||
|
self.assertEqual(1, len(objSet))
|
||||||
|
obj_spec = objSet[0]
|
||||||
|
self.assertEqual(moref, obj_spec.obj)
|
||||||
|
self.assertEqual([], obj_spec.selectSet)
|
||||||
|
self.assertFalse(obj_spec.skip)
|
||||||
|
|
||||||
|
return retrieve_result
|
||||||
|
|
||||||
|
vim.RetrievePropertiesEx.side_effect = \
|
||||||
|
vim_RetrievePropertiesEx_side_effect
|
||||||
|
|
||||||
|
res = vim_util.get_object_properties(vim, moref, None)
|
||||||
|
self.assertEqual(1, vim.RetrievePropertiesEx.call_count)
|
||||||
|
self.assertTrue(res is retrieve_result.objects)
|
||||||
|
cancel_retrieval.assert_called_once_with(vim, retrieve_result)
|
||||||
|
|
||||||
|
def test_get_token(self):
|
||||||
|
retrieve_result = object()
|
||||||
|
self.assertFalse(vim_util._get_token(retrieve_result))
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.vim_util._get_token')
|
||||||
|
def test_cancel_retrieval(self, get_token):
|
||||||
|
token = mock.Mock()
|
||||||
|
get_token.return_value = token
|
||||||
|
vim = mock.Mock()
|
||||||
|
retrieve_result = mock.Mock()
|
||||||
|
vim_util.cancel_retrieval(vim, retrieve_result)
|
||||||
|
get_token.assert_called_once_with(retrieve_result)
|
||||||
|
vim.CancelRetrievePropertiesEx.assert_called_once_with(
|
||||||
|
vim.service_content.propertyCollector, token=token)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.vim_util._get_token')
|
||||||
|
def test_continue_retrieval(self, get_token):
|
||||||
|
token = mock.Mock()
|
||||||
|
get_token.return_value = token
|
||||||
|
vim = mock.Mock()
|
||||||
|
retrieve_result = mock.Mock()
|
||||||
|
vim_util.continue_retrieval(vim, retrieve_result)
|
||||||
|
get_token.assert_called_once_with(retrieve_result)
|
||||||
|
vim.ContinueRetrievePropertiesEx.assert_called_once_with(
|
||||||
|
vim.service_content.propertyCollector, token=token)
|
||||||
|
|
||||||
|
@mock.patch('oslo_vmware.vim_util.get_object_properties')
|
||||||
|
def test_get_object_property(self, get_object_properties):
|
||||||
|
prop = mock.Mock()
|
||||||
|
prop.val = "ubuntu-12.04"
|
||||||
|
properties = mock.Mock()
|
||||||
|
properties.propSet = [prop]
|
||||||
|
properties_list = [properties]
|
||||||
|
get_object_properties.return_value = properties_list
|
||||||
|
vim = mock.Mock()
|
||||||
|
moref = mock.Mock()
|
||||||
|
property_name = 'name'
|
||||||
|
val = vim_util.get_object_property(vim, moref, property_name)
|
||||||
|
self.assertEqual(prop.val, val)
|
||||||
|
get_object_properties.assert_called_once_with(
|
||||||
|
vim, moref, [property_name])
|
||||||
|
|
||||||
|
def test_find_extension(self):
|
||||||
|
vim = mock.Mock()
|
||||||
|
ret = vim_util.find_extension(vim, 'fake-key')
|
||||||
|
self.assertIsNotNone(ret)
|
||||||
|
service_content = vim.service_content
|
||||||
|
vim.client.service.FindExtension.assert_called_once_with(
|
||||||
|
service_content.extensionManager, 'fake-key')
|
||||||
|
|
||||||
|
def test_register_extension(self):
|
||||||
|
vim = mock.Mock()
|
||||||
|
ret = vim_util.register_extension(vim, 'fake-key', 'fake-type')
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
service_content = vim.service_content
|
||||||
|
vim.client.service.RegisterExtension.assert_called_once_with(
|
||||||
|
service_content.extensionManager, mock.ANY)
|
||||||
|
|
||||||
|
def test_get_vc_version(self):
|
||||||
|
session = mock.Mock()
|
||||||
|
expected_version = '6.0.1'
|
||||||
|
session.vim.service_content.about.version = expected_version
|
||||||
|
version = vim_util.get_vc_version(session)
|
||||||
|
self.assertEqual(expected_version, version)
|
||||||
|
expected_version = '5.5'
|
||||||
|
session.vim.service_content.about.version = expected_version
|
||||||
|
version = vim_util.get_vc_version(session)
|
||||||
|
self.assertEqual(expected_version, version)
|
||||||
|
|
||||||
|
def test_get_inventory_path_folders(self):
|
||||||
|
ObjectContent = collections.namedtuple('ObjectContent', ['propSet'])
|
||||||
|
DynamicProperty = collections.namedtuple('Property', ['name', 'val'])
|
||||||
|
|
||||||
|
obj1 = ObjectContent(propSet=[
|
||||||
|
DynamicProperty(name='Datacenter', val='dc-1'),
|
||||||
|
])
|
||||||
|
obj2 = ObjectContent(propSet=[
|
||||||
|
DynamicProperty(name='Datacenter', val='folder-2'),
|
||||||
|
])
|
||||||
|
obj3 = ObjectContent(propSet=[
|
||||||
|
DynamicProperty(name='Datacenter', val='folder-1'),
|
||||||
|
])
|
||||||
|
objects = ['foo', 'bar', obj1, obj2, obj3]
|
||||||
|
result = mock.sentinel.objects
|
||||||
|
result.objects = objects
|
||||||
|
session = mock.Mock()
|
||||||
|
session.vim.RetrievePropertiesEx = mock.Mock()
|
||||||
|
session.vim.RetrievePropertiesEx.return_value = result
|
||||||
|
entity = mock.Mock()
|
||||||
|
inv_path = vim_util.get_inventory_path(session.vim, entity, 100)
|
||||||
|
self.assertEqual('/folder-2/dc-1', inv_path)
|
||||||
|
|
||||||
|
def test_get_inventory_path_no_folder(self):
|
||||||
|
ObjectContent = collections.namedtuple('ObjectContent', ['propSet'])
|
||||||
|
DynamicProperty = collections.namedtuple('Property', ['name', 'val'])
|
||||||
|
|
||||||
|
obj1 = ObjectContent(propSet=[
|
||||||
|
DynamicProperty(name='Datacenter', val='dc-1'),
|
||||||
|
])
|
||||||
|
objects = ['foo', 'bar', obj1]
|
||||||
|
result = mock.sentinel.objects
|
||||||
|
result.objects = objects
|
||||||
|
session = mock.Mock()
|
||||||
|
session.vim.RetrievePropertiesEx = mock.Mock()
|
||||||
|
session.vim.RetrievePropertiesEx.return_value = result
|
||||||
|
entity = mock.Mock()
|
||||||
|
inv_path = vim_util.get_inventory_path(session.vim, entity, 100)
|
||||||
|
self.assertEqual('dc-1', inv_path)
|
50
oslo_vmware/vim.py
Normal file
50
oslo_vmware/vim.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_vmware import service
|
||||||
|
|
||||||
|
|
||||||
|
class Vim(service.Service):
|
||||||
|
"""Service class that provides access to the VIM API."""
|
||||||
|
|
||||||
|
def __init__(self, protocol='https', host='localhost', port=None,
|
||||||
|
wsdl_url=None, cacert=None, insecure=True):
|
||||||
|
"""Constructs a VIM service client object.
|
||||||
|
|
||||||
|
:param protocol: http or https
|
||||||
|
:param host: server IP address or host name
|
||||||
|
:param port: port for connection
|
||||||
|
:param wsdl_url: VIM WSDL url
|
||||||
|
:param cacert: Specify a CA bundle file to use in verifying a
|
||||||
|
TLS (https) server certificate.
|
||||||
|
:param insecure: Verify HTTPS connections using system certificates,
|
||||||
|
used only if cacert is not specified
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
base_url = service.Service.build_base_url(protocol, host, port)
|
||||||
|
soap_url = base_url + '/sdk'
|
||||||
|
if wsdl_url is None:
|
||||||
|
wsdl_url = soap_url + '/vimService.wsdl'
|
||||||
|
super(Vim, self).__init__(wsdl_url, soap_url, cacert, insecure)
|
||||||
|
|
||||||
|
def retrieve_service_content(self):
|
||||||
|
return self.RetrieveServiceContent(service.SERVICE_INSTANCE)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "VIM Object"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "VIM Object"
|
486
oslo_vmware/vim_util.py
Normal file
486
oslo_vmware/vim_util.py
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
The VMware API utility module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from suds import sudsobject
|
||||||
|
|
||||||
|
from oslo.utils import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
def get_moref(value, type_):
|
||||||
|
"""Get managed object reference.
|
||||||
|
|
||||||
|
:param value: value of the managed object
|
||||||
|
:param type_: type of the managed object
|
||||||
|
:returns: managed object reference with given value and type
|
||||||
|
"""
|
||||||
|
moref = sudsobject.Property(value)
|
||||||
|
moref._type = type_
|
||||||
|
return moref
|
||||||
|
|
||||||
|
|
||||||
|
def build_selection_spec(client_factory, name):
|
||||||
|
"""Builds the selection spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param name: name for the selection spec
|
||||||
|
:returns: selection spec
|
||||||
|
"""
|
||||||
|
sel_spec = client_factory.create('ns0:SelectionSpec')
|
||||||
|
sel_spec.name = name
|
||||||
|
return sel_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_traversal_spec(client_factory, name, type_, path, skip, select_set):
|
||||||
|
"""Builds the traversal spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param name: name for the traversal spec
|
||||||
|
:param type_: type of the managed object
|
||||||
|
:param path: property path of the managed object
|
||||||
|
:param skip: whether or not to filter the object identified by param path
|
||||||
|
:param select_set: set of selection specs specifying additional objects
|
||||||
|
to filter
|
||||||
|
:returns: traversal spec
|
||||||
|
"""
|
||||||
|
traversal_spec = client_factory.create('ns0:TraversalSpec')
|
||||||
|
traversal_spec.name = name
|
||||||
|
traversal_spec.type = type_
|
||||||
|
traversal_spec.path = path
|
||||||
|
traversal_spec.skip = skip
|
||||||
|
traversal_spec.selectSet = select_set
|
||||||
|
return traversal_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_recursive_traversal_spec(client_factory):
|
||||||
|
"""Builds recursive traversal spec to traverse managed object hierarchy.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:returns: recursive traversal spec
|
||||||
|
"""
|
||||||
|
visit_folders_select_spec = build_selection_spec(client_factory,
|
||||||
|
'visitFolders')
|
||||||
|
# Next hop from Datacenter
|
||||||
|
dc_to_hf = build_traversal_spec(client_factory,
|
||||||
|
'dc_to_hf',
|
||||||
|
'Datacenter',
|
||||||
|
'hostFolder',
|
||||||
|
False,
|
||||||
|
[visit_folders_select_spec])
|
||||||
|
dc_to_vmf = build_traversal_spec(client_factory,
|
||||||
|
'dc_to_vmf',
|
||||||
|
'Datacenter',
|
||||||
|
'vmFolder',
|
||||||
|
False,
|
||||||
|
[visit_folders_select_spec])
|
||||||
|
dc_to_netf = build_traversal_spec(client_factory,
|
||||||
|
'dc_to_netf',
|
||||||
|
'Datacenter',
|
||||||
|
'networkFolder',
|
||||||
|
False,
|
||||||
|
[visit_folders_select_spec])
|
||||||
|
|
||||||
|
# Next hop from HostSystem
|
||||||
|
h_to_vm = build_traversal_spec(client_factory,
|
||||||
|
'h_to_vm',
|
||||||
|
'HostSystem',
|
||||||
|
'vm',
|
||||||
|
False,
|
||||||
|
[visit_folders_select_spec])
|
||||||
|
|
||||||
|
# Next hop from ComputeResource
|
||||||
|
cr_to_h = build_traversal_spec(client_factory,
|
||||||
|
'cr_to_h',
|
||||||
|
'ComputeResource',
|
||||||
|
'host',
|
||||||
|
False,
|
||||||
|
[])
|
||||||
|
cr_to_ds = build_traversal_spec(client_factory,
|
||||||
|
'cr_to_ds',
|
||||||
|
'ComputeResource',
|
||||||
|
'datastore',
|
||||||
|
False,
|
||||||
|
[])
|
||||||
|
|
||||||
|
rp_to_rp_select_spec = build_selection_spec(client_factory, 'rp_to_rp')
|
||||||
|
rp_to_vm_select_spec = build_selection_spec(client_factory, 'rp_to_vm')
|
||||||
|
|
||||||
|
cr_to_rp = build_traversal_spec(client_factory,
|
||||||
|
'cr_to_rp',
|
||||||
|
'ComputeResource',
|
||||||
|
'resourcePool',
|
||||||
|
False,
|
||||||
|
[rp_to_rp_select_spec,
|
||||||
|
rp_to_vm_select_spec])
|
||||||
|
|
||||||
|
# Next hop from ClusterComputeResource
|
||||||
|
ccr_to_h = build_traversal_spec(client_factory,
|
||||||
|
'ccr_to_h',
|
||||||
|
'ClusterComputeResource',
|
||||||
|
'host',
|
||||||
|
False,
|
||||||
|
[])
|
||||||
|
ccr_to_ds = build_traversal_spec(client_factory,
|
||||||
|
'ccr_to_ds',
|
||||||
|
'ClusterComputeResource',
|
||||||
|
'datastore',
|
||||||
|
False,
|
||||||
|
[])
|
||||||
|
ccr_to_rp = build_traversal_spec(client_factory,
|
||||||
|
'ccr_to_rp',
|
||||||
|
'ClusterComputeResource',
|
||||||
|
'resourcePool',
|
||||||
|
False,
|
||||||
|
[rp_to_rp_select_spec,
|
||||||
|
rp_to_vm_select_spec])
|
||||||
|
# Next hop from ResourcePool
|
||||||
|
rp_to_rp = build_traversal_spec(client_factory,
|
||||||
|
'rp_to_rp',
|
||||||
|
'ResourcePool',
|
||||||
|
'resourcePool',
|
||||||
|
False,
|
||||||
|
[rp_to_rp_select_spec,
|
||||||
|
rp_to_vm_select_spec])
|
||||||
|
rp_to_vm = build_traversal_spec(client_factory,
|
||||||
|
'rp_to_vm',
|
||||||
|
'ResourcePool',
|
||||||
|
'vm',
|
||||||
|
False,
|
||||||
|
[rp_to_rp_select_spec,
|
||||||
|
rp_to_vm_select_spec])
|
||||||
|
|
||||||
|
# Get the assorted traversal spec which takes care of the objects to
|
||||||
|
# be searched for from the rootFolder
|
||||||
|
traversal_spec = build_traversal_spec(client_factory,
|
||||||
|
'visitFolders',
|
||||||
|
'Folder',
|
||||||
|
'childEntity',
|
||||||
|
False,
|
||||||
|
[visit_folders_select_spec,
|
||||||
|
h_to_vm,
|
||||||
|
dc_to_hf,
|
||||||
|
dc_to_vmf,
|
||||||
|
dc_to_netf,
|
||||||
|
cr_to_ds,
|
||||||
|
cr_to_h,
|
||||||
|
cr_to_rp,
|
||||||
|
ccr_to_h,
|
||||||
|
ccr_to_ds,
|
||||||
|
ccr_to_rp,
|
||||||
|
rp_to_rp,
|
||||||
|
rp_to_vm])
|
||||||
|
return traversal_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_property_spec(client_factory, type_='VirtualMachine',
|
||||||
|
properties_to_collect=None, all_properties=False):
|
||||||
|
"""Builds the property spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param type_: type of the managed object
|
||||||
|
:param properties_to_collect: names of the managed object properties to be
|
||||||
|
collected while traversal filtering
|
||||||
|
:param all_properties: whether all properties of the managed object need
|
||||||
|
to be collected
|
||||||
|
:returns: property spec
|
||||||
|
"""
|
||||||
|
if not properties_to_collect:
|
||||||
|
properties_to_collect = ['name']
|
||||||
|
|
||||||
|
property_spec = client_factory.create('ns0:PropertySpec')
|
||||||
|
property_spec.all = all_properties
|
||||||
|
property_spec.pathSet = properties_to_collect
|
||||||
|
property_spec.type = type_
|
||||||
|
return property_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_object_spec(client_factory, root_folder, traversal_specs):
|
||||||
|
"""Builds the object spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param root_folder: root folder reference; the starting point of traversal
|
||||||
|
:param traversal_specs: filter specs required for traversal
|
||||||
|
:returns: object spec
|
||||||
|
"""
|
||||||
|
object_spec = client_factory.create('ns0:ObjectSpec')
|
||||||
|
object_spec.obj = root_folder
|
||||||
|
object_spec.skip = False
|
||||||
|
object_spec.selectSet = traversal_specs
|
||||||
|
return object_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_property_filter_spec(client_factory, property_specs, object_specs):
|
||||||
|
"""Builds the property filter spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param property_specs: property specs to be collected for filtered objects
|
||||||
|
:param object_specs: object specs to identify objects to be filtered
|
||||||
|
:returns: property filter spec
|
||||||
|
"""
|
||||||
|
property_filter_spec = client_factory.create('ns0:PropertyFilterSpec')
|
||||||
|
property_filter_spec.propSet = property_specs
|
||||||
|
property_filter_spec.objectSet = object_specs
|
||||||
|
return property_filter_spec
|
||||||
|
|
||||||
|
|
||||||
|
def get_objects(vim, type_, max_objects, properties_to_collect=None,
|
||||||
|
all_properties=False):
|
||||||
|
"""Get all managed object references of the given type.
|
||||||
|
|
||||||
|
It is the caller's responsibility to continue or cancel retrieval.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param type_: type of the managed object
|
||||||
|
:param max_objects: maximum number of objects that should be returned in
|
||||||
|
a single call
|
||||||
|
:param properties_to_collect: names of the managed object properties to be
|
||||||
|
collected
|
||||||
|
:param all_properties: whether all properties of the managed object need to
|
||||||
|
be collected
|
||||||
|
:returns: all managed object references of the given type
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
if not properties_to_collect:
|
||||||
|
properties_to_collect = ['name']
|
||||||
|
|
||||||
|
client_factory = vim.client.factory
|
||||||
|
recur_trav_spec = build_recursive_traversal_spec(client_factory)
|
||||||
|
object_spec = build_object_spec(client_factory,
|
||||||
|
vim.service_content.rootFolder,
|
||||||
|
[recur_trav_spec])
|
||||||
|
property_spec = build_property_spec(
|
||||||
|
client_factory,
|
||||||
|
type_=type_,
|
||||||
|
properties_to_collect=properties_to_collect,
|
||||||
|
all_properties=all_properties)
|
||||||
|
property_filter_spec = build_property_filter_spec(client_factory,
|
||||||
|
[property_spec],
|
||||||
|
[object_spec])
|
||||||
|
options = client_factory.create('ns0:RetrieveOptions')
|
||||||
|
options.maxObjects = max_objects
|
||||||
|
return vim.RetrievePropertiesEx(vim.service_content.propertyCollector,
|
||||||
|
specSet=[property_filter_spec],
|
||||||
|
options=options)
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_properties(vim, moref, properties_to_collect):
|
||||||
|
"""Get properties of the given managed object.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param moref: managed object reference
|
||||||
|
:param properties_to_collect: names of the managed object properties to be
|
||||||
|
collected
|
||||||
|
:returns: properties of the given managed object
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
if moref is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
client_factory = vim.client.factory
|
||||||
|
all_properties = (properties_to_collect is None or
|
||||||
|
len(properties_to_collect) == 0)
|
||||||
|
property_spec = build_property_spec(
|
||||||
|
client_factory,
|
||||||
|
type_=moref._type,
|
||||||
|
properties_to_collect=properties_to_collect,
|
||||||
|
all_properties=all_properties)
|
||||||
|
object_spec = build_object_spec(client_factory, moref, [])
|
||||||
|
property_filter_spec = build_property_filter_spec(client_factory,
|
||||||
|
[property_spec],
|
||||||
|
[object_spec])
|
||||||
|
|
||||||
|
options = client_factory.create('ns0:RetrieveOptions')
|
||||||
|
options.maxObjects = 1
|
||||||
|
retrieve_result = vim.RetrievePropertiesEx(
|
||||||
|
vim.service_content.propertyCollector,
|
||||||
|
specSet=[property_filter_spec],
|
||||||
|
options=options)
|
||||||
|
cancel_retrieval(vim, retrieve_result)
|
||||||
|
return retrieve_result.objects
|
||||||
|
|
||||||
|
|
||||||
|
def _get_token(retrieve_result):
|
||||||
|
"""Get token from result to obtain next set of results.
|
||||||
|
|
||||||
|
:retrieve_result: Result of RetrievePropertiesEx API call
|
||||||
|
:returns: token to obtain next set of results; None if no more results.
|
||||||
|
"""
|
||||||
|
return getattr(retrieve_result, 'token', None)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_retrieval(vim, retrieve_result):
|
||||||
|
"""Cancels the retrieve operation if necessary.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param retrieve_result: result of RetrievePropertiesEx API call
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
token = _get_token(retrieve_result)
|
||||||
|
if token:
|
||||||
|
collector = vim.service_content.propertyCollector
|
||||||
|
vim.CancelRetrievePropertiesEx(collector, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def continue_retrieval(vim, retrieve_result):
|
||||||
|
"""Continue retrieving results, if available.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param retrieve_result: result of RetrievePropertiesEx API call
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
token = _get_token(retrieve_result)
|
||||||
|
if token:
|
||||||
|
collector = vim.service_content.propertyCollector
|
||||||
|
return vim.ContinueRetrievePropertiesEx(collector, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_property(vim, moref, property_name):
|
||||||
|
"""Get property of the given managed object.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param moref: managed object reference
|
||||||
|
:param property_name: name of the property to be retrieved
|
||||||
|
:returns: property of the given managed object
|
||||||
|
:raises: VimException, VimFaultException, VimAttributeException,
|
||||||
|
VimSessionOverLoadException, VimConnectionException
|
||||||
|
"""
|
||||||
|
props = get_object_properties(vim, moref, [property_name])
|
||||||
|
prop_val = None
|
||||||
|
if props:
|
||||||
|
prop = None
|
||||||
|
if hasattr(props[0], 'propSet'):
|
||||||
|
# propSet will be set only if the server provides value
|
||||||
|
# for the field
|
||||||
|
prop = props[0].propSet
|
||||||
|
if prop:
|
||||||
|
prop_val = prop[0].val
|
||||||
|
return prop_val
|
||||||
|
|
||||||
|
|
||||||
|
def find_extension(vim, key):
|
||||||
|
"""Looks for an existing extension.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param key: the key to search for
|
||||||
|
:returns: the data object Extension or None
|
||||||
|
"""
|
||||||
|
extension_manager = vim.service_content.extensionManager
|
||||||
|
return vim.client.service.FindExtension(extension_manager, key)
|
||||||
|
|
||||||
|
|
||||||
|
def register_extension(vim, key, type, label='OpenStack',
|
||||||
|
summary='OpenStack services', version='1.0'):
|
||||||
|
"""Create a new extention.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param key: the key for the extension
|
||||||
|
:param type: Managed entity type, as defined by the extension. This
|
||||||
|
matches the type field in the configuration about a
|
||||||
|
virtual machine or vApp
|
||||||
|
:param label: Display label
|
||||||
|
:param summary: Summary description
|
||||||
|
:param version: Extension version number as a dot-separated string
|
||||||
|
"""
|
||||||
|
extension_manager = vim.service_content.extensionManager
|
||||||
|
client_factory = vim.client.factory
|
||||||
|
os_ext = client_factory.create('ns0:Extension')
|
||||||
|
os_ext.key = key
|
||||||
|
entity_info = client_factory.create('ns0:ExtManagedEntityInfo')
|
||||||
|
entity_info.type = type
|
||||||
|
os_ext.managedEntityInfo = [entity_info]
|
||||||
|
os_ext.version = version
|
||||||
|
desc = client_factory.create('ns0:Description')
|
||||||
|
desc.label = label
|
||||||
|
desc.summary = summary
|
||||||
|
os_ext.description = desc
|
||||||
|
os_ext.lastHeartbeatTime = timeutils.strtime()
|
||||||
|
vim.client.service.RegisterExtension(extension_manager, os_ext)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vc_version(session):
|
||||||
|
"""Return the dot-separated vCenter version string. For example, "1.2".
|
||||||
|
|
||||||
|
:param session: vCenter soap session
|
||||||
|
:return: vCenter version
|
||||||
|
"""
|
||||||
|
return session.vim.service_content.about.version
|
||||||
|
|
||||||
|
|
||||||
|
def get_inventory_path(vim, entity_ref, max_objects=100):
|
||||||
|
"""Get the inventory path of a managed entity.
|
||||||
|
|
||||||
|
:param vim: Vim object
|
||||||
|
:param entity_ref: managed entity reference
|
||||||
|
:param max_objects: maximum number of objects that should be returned in
|
||||||
|
a single call
|
||||||
|
:return: inventory path of the entity_ref
|
||||||
|
"""
|
||||||
|
client_factory = vim.client.factory
|
||||||
|
property_collector = vim.service_content.propertyCollector
|
||||||
|
|
||||||
|
prop_spec = build_property_spec(client_factory, 'ManagedEntity',
|
||||||
|
['name', 'parent'])
|
||||||
|
select_set = build_selection_spec(client_factory, 'ParentTraversalSpec')
|
||||||
|
select_set = build_traversal_spec(
|
||||||
|
client_factory, 'ParentTraversalSpec', 'ManagedEntity', 'parent',
|
||||||
|
False, [select_set])
|
||||||
|
obj_spec = build_object_spec(client_factory, entity_ref, select_set)
|
||||||
|
prop_filter_spec = build_property_filter_spec(client_factory,
|
||||||
|
[prop_spec], [obj_spec])
|
||||||
|
options = client_factory.create('ns0:RetrieveOptions')
|
||||||
|
options.maxObjects = max_objects
|
||||||
|
retrieve_result = vim.RetrievePropertiesEx(
|
||||||
|
property_collector,
|
||||||
|
specSet=[prop_filter_spec],
|
||||||
|
options=options)
|
||||||
|
entity_name = None
|
||||||
|
propSet = None
|
||||||
|
path = ""
|
||||||
|
while retrieve_result:
|
||||||
|
for obj in retrieve_result.objects:
|
||||||
|
if hasattr(obj, 'propSet'):
|
||||||
|
propSet = obj.propSet
|
||||||
|
if len(propSet) >= 1 and not entity_name:
|
||||||
|
entity_name = propSet[0].val
|
||||||
|
elif len(propSet) >= 1:
|
||||||
|
path = '%s/%s' % (propSet[0].val, path)
|
||||||
|
retrieve_result = continue_retrieval(vim, retrieve_result)
|
||||||
|
# NOTE(arnaud): slice to exclude the root folder from the result.
|
||||||
|
if propSet is not None and len(propSet) > 0:
|
||||||
|
path = path[len(propSet[0].val):]
|
||||||
|
if entity_name is None:
|
||||||
|
entity_name = ""
|
||||||
|
return '%s%s' % (path, entity_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_http_service_request_spec(client_factory, method, uri):
|
||||||
|
"""Build a HTTP service request spec.
|
||||||
|
|
||||||
|
:param client_factory: factory to get API input specs
|
||||||
|
:param method: HTTP method (GET, POST, PUT)
|
||||||
|
:param uri: target URL
|
||||||
|
"""
|
||||||
|
http_service_request_spec = client_factory.create(
|
||||||
|
'ns0:SessionManagerHttpServiceRequestSpec')
|
||||||
|
http_service_request_spec.method = method
|
||||||
|
http_service_request_spec.url = uri
|
||||||
|
return http_service_request_spec
|
@ -22,6 +22,7 @@ classifier =
|
|||||||
[files]
|
[files]
|
||||||
packages =
|
packages =
|
||||||
oslo
|
oslo
|
||||||
|
oslo_vmware
|
||||||
namespace_packages =
|
namespace_packages =
|
||||||
oslo
|
oslo
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ from oslo.utils import units
|
|||||||
from oslo.vmware import constants
|
from oslo.vmware import constants
|
||||||
from oslo.vmware.objects import datastore
|
from oslo.vmware.objects import datastore
|
||||||
from oslo.vmware import vim_util
|
from oslo.vmware import vim_util
|
||||||
|
from oslo_vmware import vim_util as new_vim_util
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ class DatastoreTestCase(base.TestCase):
|
|||||||
session.invoke_api.return_value = summary
|
session.invoke_api.return_value = summary
|
||||||
ret = ds.get_summary(session)
|
ret = ds.get_summary(session)
|
||||||
self.assertEqual(summary, ret)
|
self.assertEqual(summary, ret)
|
||||||
session.invoke_api.assert_called_once_with(vim_util,
|
session.invoke_api.assert_called_once_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
session.vim,
|
session.vim,
|
||||||
ds.ref, 'summary')
|
ds.ref, 'summary')
|
||||||
|
@ -25,8 +25,7 @@ import suds
|
|||||||
|
|
||||||
from oslo.vmware import api
|
from oslo.vmware import api
|
||||||
from oslo.vmware import exceptions
|
from oslo.vmware import exceptions
|
||||||
from oslo.vmware import pbm
|
from oslo_vmware import vim_util as new_vim_util
|
||||||
from oslo.vmware import vim_util
|
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -105,7 +104,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(VMwareAPISessionTest, self).setUp()
|
super(VMwareAPISessionTest, self).setUp()
|
||||||
patcher = mock.patch('oslo.vmware.vim.Vim')
|
patcher = mock.patch('oslo_vmware.vim.Vim')
|
||||||
self.addCleanup(patcher.stop)
|
self.addCleanup(patcher.stop)
|
||||||
self.VimMock = patcher.start()
|
self.VimMock = patcher.start()
|
||||||
self.VimMock.side_effect = lambda *args, **kw: mock.MagicMock()
|
self.VimMock.side_effect = lambda *args, **kw: mock.MagicMock()
|
||||||
@ -134,7 +133,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
cacert=self.cert_mock,
|
cacert=self.cert_mock,
|
||||||
insecure=False)
|
insecure=False)
|
||||||
|
|
||||||
@mock.patch.object(pbm, 'Pbm')
|
@mock.patch('oslo_vmware.pbm.Pbm')
|
||||||
def test_pbm(self, pbm_mock):
|
def test_pbm(self, pbm_mock):
|
||||||
api_session = self._create_api_session(True)
|
api_session = self._create_api_session(True)
|
||||||
vim_obj = api_session.vim
|
vim_obj = api_session.vim
|
||||||
@ -372,7 +371,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
ret = api_session.wait_for_task(task)
|
ret = api_session.wait_for_task(task)
|
||||||
self.assertEqual('success', ret.state)
|
self.assertEqual('success', ret.state)
|
||||||
self.assertEqual(100, ret.progress)
|
self.assertEqual(100, ret.progress)
|
||||||
api_session.invoke_api.assert_called_with(vim_util,
|
api_session.invoke_api.assert_called_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
api_session.vim, task,
|
api_session.vim, task,
|
||||||
'info')
|
'info')
|
||||||
@ -397,7 +396,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
self.assertRaises(exceptions.VMwareDriverException,
|
self.assertRaises(exceptions.VMwareDriverException,
|
||||||
api_session.wait_for_task,
|
api_session.wait_for_task,
|
||||||
task)
|
task)
|
||||||
api_session.invoke_api.assert_called_with(vim_util,
|
api_session.invoke_api.assert_called_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
api_session.vim, task,
|
api_session.vim, task,
|
||||||
'info')
|
'info')
|
||||||
@ -413,7 +412,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
self.assertRaises(exceptions.VimException,
|
self.assertRaises(exceptions.VimException,
|
||||||
api_session.wait_for_task,
|
api_session.wait_for_task,
|
||||||
task)
|
task)
|
||||||
api_session.invoke_api.assert_called_once_with(vim_util,
|
api_session.invoke_api.assert_called_once_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
api_session.vim, task,
|
api_session.vim, task,
|
||||||
'info')
|
'info')
|
||||||
@ -430,7 +429,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
lease = mock.Mock()
|
lease = mock.Mock()
|
||||||
with mock.patch.object(greenthread, 'sleep'):
|
with mock.patch.object(greenthread, 'sleep'):
|
||||||
api_session.wait_for_lease_ready(lease)
|
api_session.wait_for_lease_ready(lease)
|
||||||
api_session.invoke_api.assert_called_with(vim_util,
|
api_session.invoke_api.assert_called_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
api_session.vim, lease,
|
api_session.vim, lease,
|
||||||
'state')
|
'state')
|
||||||
@ -449,9 +448,9 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
self.assertRaises(exceptions.VimException,
|
self.assertRaises(exceptions.VimException,
|
||||||
api_session.wait_for_lease_ready,
|
api_session.wait_for_lease_ready,
|
||||||
lease)
|
lease)
|
||||||
exp_calls = [mock.call(vim_util, 'get_object_property',
|
exp_calls = [mock.call(new_vim_util, 'get_object_property',
|
||||||
api_session.vim, lease, 'state')] * 2
|
api_session.vim, lease, 'state')] * 2
|
||||||
exp_calls.append(mock.call(vim_util, 'get_object_property',
|
exp_calls.append(mock.call(new_vim_util, 'get_object_property',
|
||||||
api_session.vim, lease, 'error'))
|
api_session.vim, lease, 'error'))
|
||||||
self.assertEqual(exp_calls, api_session.invoke_api.call_args_list)
|
self.assertEqual(exp_calls, api_session.invoke_api.call_args_list)
|
||||||
|
|
||||||
@ -466,7 +465,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
self.assertRaises(exceptions.VimException,
|
self.assertRaises(exceptions.VimException,
|
||||||
api_session.wait_for_lease_ready,
|
api_session.wait_for_lease_ready,
|
||||||
lease)
|
lease)
|
||||||
api_session.invoke_api.assert_called_once_with(vim_util,
|
api_session.invoke_api.assert_called_once_with(new_vim_util,
|
||||||
'get_object_property',
|
'get_object_property',
|
||||||
api_session.vim,
|
api_session.vim,
|
||||||
lease, 'state')
|
lease, 'state')
|
||||||
@ -480,7 +479,7 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
api_session.wait_for_lease_ready,
|
api_session.wait_for_lease_ready,
|
||||||
lease)
|
lease)
|
||||||
api_session.invoke_api.assert_called_once_with(
|
api_session.invoke_api.assert_called_once_with(
|
||||||
vim_util, 'get_object_property', api_session.vim, lease,
|
new_vim_util, 'get_object_property', api_session.vim, lease,
|
||||||
'state')
|
'state')
|
||||||
|
|
||||||
def _poll_task_well_known_exceptions(self, fault,
|
def _poll_task_well_known_exceptions(self, fault,
|
||||||
@ -506,10 +505,6 @@ class VMwareAPISessionTest(base.TestCase):
|
|||||||
api_session._poll_task,
|
api_session._poll_task,
|
||||||
'fake-task')
|
'fake-task')
|
||||||
|
|
||||||
def test_poll_task_well_known_exceptions(self):
|
|
||||||
for k, v in six.iteritems(exceptions._fault_classes_registry):
|
|
||||||
self._poll_task_well_known_exceptions(k, v)
|
|
||||||
|
|
||||||
def test_poll_task_unknown_exception(self):
|
def test_poll_task_unknown_exception(self):
|
||||||
_unknown_exceptions = {
|
_unknown_exceptions = {
|
||||||
'NoDiskSpace': exceptions.VMwareDriverException,
|
'NoDiskSpace': exceptions.VMwareDriverException,
|
||||||
|
@ -26,6 +26,7 @@ import mock
|
|||||||
from oslo.vmware import exceptions
|
from oslo.vmware import exceptions
|
||||||
from oslo.vmware import image_transfer
|
from oslo.vmware import image_transfer
|
||||||
from oslo.vmware import rw_handles
|
from oslo.vmware import rw_handles
|
||||||
|
from oslo_vmware import image_transfer as new_image_transfer
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -191,9 +192,9 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
"""Tests for image_transfer utility methods."""
|
"""Tests for image_transfer utility methods."""
|
||||||
|
|
||||||
@mock.patch.object(timeout, 'Timeout')
|
@mock.patch.object(timeout, 'Timeout')
|
||||||
@mock.patch.object(image_transfer, 'ImageWriter')
|
@mock.patch('oslo_vmware.image_transfer.ImageWriter')
|
||||||
@mock.patch.object(image_transfer, 'FileReadWriteTask')
|
@mock.patch('oslo_vmware.image_transfer.FileReadWriteTask')
|
||||||
@mock.patch.object(image_transfer, 'BlockingQueue')
|
@mock.patch('oslo_vmware.image_transfer.BlockingQueue')
|
||||||
def test_start_transfer(self, fake_BlockingQueue, fake_FileReadWriteTask,
|
def test_start_transfer(self, fake_BlockingQueue, fake_FileReadWriteTask,
|
||||||
fake_ImageWriter, fake_Timeout):
|
fake_ImageWriter, fake_Timeout):
|
||||||
|
|
||||||
@ -220,7 +221,8 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
fake_Timeout.return_value = fake_timer
|
fake_Timeout.return_value = fake_timer
|
||||||
|
|
||||||
for write_file_handle in write_file_handles:
|
for write_file_handle in write_file_handles:
|
||||||
image_transfer._start_transfer(context,
|
new_image_transfer._start_transfer(
|
||||||
|
context,
|
||||||
timeout_secs,
|
timeout_secs,
|
||||||
read_file_handle,
|
read_file_handle,
|
||||||
max_data_size,
|
max_data_size,
|
||||||
@ -257,44 +259,9 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
|
|
||||||
write_file_handle1.close.assert_called_once()
|
write_file_handle1.close.assert_called_once()
|
||||||
|
|
||||||
@mock.patch.object(image_transfer, 'FileReadWriteTask')
|
@mock.patch('oslo_vmware.rw_handles.FileWriteHandle')
|
||||||
@mock.patch.object(image_transfer, 'BlockingQueue')
|
@mock.patch('oslo_vmware.rw_handles.ImageReadHandle')
|
||||||
def test_start_transfer_with_no_image_destination(self, fake_BlockingQueue,
|
@mock.patch('oslo_vmware.image_transfer._start_transfer')
|
||||||
fake_FileReadWriteTask):
|
|
||||||
|
|
||||||
context = mock.Mock()
|
|
||||||
read_file_handle = mock.Mock()
|
|
||||||
write_file_handle = None
|
|
||||||
image_service = None
|
|
||||||
image_id = None
|
|
||||||
timeout_secs = 10
|
|
||||||
image_meta = {}
|
|
||||||
blocking_queue_size = 10
|
|
||||||
max_data_size = 30
|
|
||||||
blocking_queue = mock.Mock()
|
|
||||||
|
|
||||||
fake_BlockingQueue.return_value = blocking_queue
|
|
||||||
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
image_transfer._start_transfer,
|
|
||||||
context,
|
|
||||||
timeout_secs,
|
|
||||||
read_file_handle,
|
|
||||||
max_data_size,
|
|
||||||
write_file_handle=write_file_handle,
|
|
||||||
image_service=image_service,
|
|
||||||
image_id=image_id,
|
|
||||||
image_meta=image_meta)
|
|
||||||
|
|
||||||
fake_BlockingQueue.assert_called_once_with(blocking_queue_size,
|
|
||||||
max_data_size)
|
|
||||||
|
|
||||||
fake_FileReadWriteTask.assert_called_once_with(read_file_handle,
|
|
||||||
blocking_queue)
|
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.rw_handles.FileWriteHandle')
|
|
||||||
@mock.patch('oslo.vmware.rw_handles.ImageReadHandle')
|
|
||||||
@mock.patch.object(image_transfer, '_start_transfer')
|
|
||||||
def test_download_flat_image(
|
def test_download_flat_image(
|
||||||
self,
|
self,
|
||||||
fake_transfer,
|
fake_transfer,
|
||||||
@ -355,8 +322,8 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
image_size,
|
image_size,
|
||||||
write_file_handle=fake_FileWriteHandle)
|
write_file_handle=fake_FileWriteHandle)
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.rw_handles.VmdkWriteHandle')
|
@mock.patch('oslo_vmware.rw_handles.VmdkWriteHandle')
|
||||||
@mock.patch.object(image_transfer, '_start_transfer')
|
@mock.patch('oslo_vmware.image_transfer._start_transfer')
|
||||||
def test_download_stream_optimized_data(self, fake_transfer,
|
def test_download_stream_optimized_data(self, fake_transfer,
|
||||||
fake_rw_handles_VmdkWriteHandle):
|
fake_rw_handles_VmdkWriteHandle):
|
||||||
|
|
||||||
@ -405,8 +372,8 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
|
|
||||||
fake_VmdkWriteHandle.get_imported_vm.assert_called_once()
|
fake_VmdkWriteHandle.get_imported_vm.assert_called_once()
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.rw_handles.ImageReadHandle')
|
@mock.patch('oslo_vmware.rw_handles.ImageReadHandle')
|
||||||
@mock.patch.object(image_transfer, 'download_stream_optimized_data')
|
@mock.patch('oslo_vmware.image_transfer.download_stream_optimized_data')
|
||||||
def test_download_stream_optimized_image(
|
def test_download_stream_optimized_image(
|
||||||
self, fake_download_stream_optimized_data,
|
self, fake_download_stream_optimized_data,
|
||||||
fake_rw_handles_ImageReadHandle):
|
fake_rw_handles_ImageReadHandle):
|
||||||
@ -459,8 +426,8 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
vm_import_spec=vm_import_spec,
|
vm_import_spec=vm_import_spec,
|
||||||
image_size=image_size)
|
image_size=image_size)
|
||||||
|
|
||||||
@mock.patch.object(image_transfer, '_start_transfer')
|
@mock.patch('oslo_vmware.image_transfer._start_transfer')
|
||||||
@mock.patch('oslo.vmware.rw_handles.VmdkReadHandle')
|
@mock.patch('oslo_vmware.rw_handles.VmdkReadHandle')
|
||||||
def test_copy_stream_optimized_disk(
|
def test_copy_stream_optimized_disk(
|
||||||
self, vmdk_read_handle, start_transfer):
|
self, vmdk_read_handle, start_transfer):
|
||||||
|
|
||||||
@ -488,8 +455,8 @@ class ImageTransferUtilityTest(base.TestCase):
|
|||||||
context, timeout, read_handle, vmdk_size,
|
context, timeout, read_handle, vmdk_size,
|
||||||
write_file_handle=write_handle)
|
write_file_handle=write_handle)
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.rw_handles.VmdkReadHandle')
|
@mock.patch('oslo_vmware.rw_handles.VmdkReadHandle')
|
||||||
@mock.patch.object(image_transfer, '_start_transfer')
|
@mock.patch('oslo_vmware.image_transfer._start_transfer')
|
||||||
def test_upload_image(self, fake_transfer, fake_rw_handles_VmdkReadHandle):
|
def test_upload_image(self, fake_transfer, fake_rw_handles_VmdkReadHandle):
|
||||||
|
|
||||||
context = mock.Mock()
|
context = mock.Mock()
|
||||||
|
@ -24,6 +24,7 @@ import six.moves.urllib.parse as urlparse
|
|||||||
import six.moves.urllib.request as urllib
|
import six.moves.urllib.request as urllib
|
||||||
|
|
||||||
from oslo.vmware import pbm
|
from oslo.vmware import pbm
|
||||||
|
from oslo_vmware import pbm as new_pbm
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ class PBMUtilityTest(base.TestCase):
|
|||||||
profile.name = name
|
profile.name = name
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
@mock.patch.object(pbm, 'get_all_profiles')
|
@mock.patch('oslo_vmware.pbm.get_all_profiles')
|
||||||
def test_get_profile_id_by_name(self, get_all_profiles):
|
def test_get_profile_id_by_name(self, get_all_profiles):
|
||||||
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
||||||
for i in range(0, 10)]
|
for i in range(0, 10)]
|
||||||
@ -82,7 +83,7 @@ class PBMUtilityTest(base.TestCase):
|
|||||||
self.assertEqual(exp_profile_id, profile_id)
|
self.assertEqual(exp_profile_id, profile_id)
|
||||||
get_all_profiles.assert_called_once_with(session)
|
get_all_profiles.assert_called_once_with(session)
|
||||||
|
|
||||||
@mock.patch.object(pbm, 'get_all_profiles')
|
@mock.patch('oslo_vmware.pbm.get_all_profiles')
|
||||||
def test_get_profile_id_by_name_with_invalid_profile(self,
|
def test_get_profile_id_by_name_with_invalid_profile(self,
|
||||||
get_all_profiles):
|
get_all_profiles):
|
||||||
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
profiles = [self._create_profile(str(i), 'profile-%d' % i)
|
||||||
@ -155,7 +156,7 @@ class PBMUtilityTest(base.TestCase):
|
|||||||
self.assertIsNone(wsdl)
|
self.assertIsNone(wsdl)
|
||||||
|
|
||||||
def expected_wsdl(version):
|
def expected_wsdl(version):
|
||||||
driver_abs_dir = os.path.abspath(os.path.dirname(pbm.__file__))
|
driver_abs_dir = os.path.abspath(os.path.dirname(new_pbm.__file__))
|
||||||
path = os.path.join(driver_abs_dir, 'wsdl', version,
|
path = os.path.join(driver_abs_dir, 'wsdl', version,
|
||||||
'pbmService.wsdl')
|
'pbmService.wsdl')
|
||||||
return urlparse.urljoin('file:', urllib.pathname2url(path))
|
return urlparse.urljoin('file:', urllib.pathname2url(path))
|
||||||
|
@ -22,7 +22,7 @@ import six
|
|||||||
|
|
||||||
from oslo.vmware import exceptions
|
from oslo.vmware import exceptions
|
||||||
from oslo.vmware import rw_handles
|
from oslo.vmware import rw_handles
|
||||||
from oslo.vmware import vim_util
|
from oslo_vmware import vim_util as new_vim_util
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ class VmdkWriteHandleTest(base.TestCase):
|
|||||||
100)
|
100)
|
||||||
|
|
||||||
def session_invoke_api_side_effect(module, method, *args, **kwargs):
|
def session_invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
if module == vim_util and method == 'get_object_property':
|
if module == new_vim_util and method == 'get_object_property':
|
||||||
return 'ready'
|
return 'ready'
|
||||||
self.assertEqual(session.vim, module)
|
self.assertEqual(session.vim, module)
|
||||||
self.assertEqual('HttpNfcLeaseComplete', method)
|
self.assertEqual('HttpNfcLeaseComplete', method)
|
||||||
@ -262,7 +262,7 @@ class VmdkReadHandleTest(base.TestCase):
|
|||||||
100)
|
100)
|
||||||
|
|
||||||
def session_invoke_api_side_effect(module, method, *args, **kwargs):
|
def session_invoke_api_side_effect(module, method, *args, **kwargs):
|
||||||
if module == vim_util and method == 'get_object_property':
|
if module == new_vim_util and method == 'get_object_property':
|
||||||
return 'ready'
|
return 'ready'
|
||||||
self.assertEqual(session.vim, module)
|
self.assertEqual(session.vim, module)
|
||||||
self.assertEqual('HttpNfcLeaseComplete', method)
|
self.assertEqual('HttpNfcLeaseComplete', method)
|
||||||
|
@ -21,7 +21,6 @@ import suds
|
|||||||
|
|
||||||
from oslo.vmware import exceptions
|
from oslo.vmware import exceptions
|
||||||
from oslo.vmware import service
|
from oslo.vmware import service
|
||||||
from oslo.vmware import vim_util
|
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -287,7 +286,7 @@ class ServiceTest(base.TestCase):
|
|||||||
svc_obj.powerOn,
|
svc_obj.powerOn,
|
||||||
managed_object)
|
managed_object)
|
||||||
|
|
||||||
@mock.patch.object(vim_util, 'get_moref', return_value=None)
|
@mock.patch('oslo_vmware.vim_util.get_moref', return_value=None)
|
||||||
def test_request_handler_no_value(self, mock_moref):
|
def test_request_handler_no_value(self, mock_moref):
|
||||||
managed_object = 'VirtualMachine'
|
managed_object = 'VirtualMachine'
|
||||||
svc_obj = service.Service()
|
svc_obj = service.Service()
|
||||||
|
@ -20,9 +20,9 @@ Unit tests for classes to invoke VMware VI SOAP calls.
|
|||||||
import mock
|
import mock
|
||||||
from oslo_i18n import fixture as i18n_fixture
|
from oslo_i18n import fixture as i18n_fixture
|
||||||
|
|
||||||
from oslo.vmware._i18n import _
|
|
||||||
from oslo.vmware import exceptions
|
from oslo.vmware import exceptions
|
||||||
from oslo.vmware import vim
|
from oslo.vmware import vim
|
||||||
|
from oslo_vmware._i18n import _
|
||||||
from tests import base
|
from tests import base
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
self.assertFalse(traversal_spec.skip)
|
self.assertFalse(traversal_spec.skip)
|
||||||
self.assertEqual("Datacenter", traversal_spec.type)
|
self.assertEqual("Datacenter", traversal_spec.type)
|
||||||
|
|
||||||
@mock.patch.object(vim_util, 'build_selection_spec')
|
@mock.patch('oslo_vmware.vim_util.build_selection_spec')
|
||||||
def test_build_recursive_traversal_spec(self, build_selection_spec_mock):
|
def test_build_recursive_traversal_spec(self, build_selection_spec_mock):
|
||||||
sel_spec = mock.Mock()
|
sel_spec = mock.Mock()
|
||||||
rp_to_rp_sel_spec = mock.Mock()
|
rp_to_rp_sel_spec = mock.Mock()
|
||||||
@ -176,7 +176,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
self.assertEqual(prop_specs, filter_spec.propSet)
|
self.assertEqual(prop_specs, filter_spec.propSet)
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'oslo.vmware.vim_util.build_recursive_traversal_spec')
|
'oslo_vmware.vim_util.build_recursive_traversal_spec')
|
||||||
def test_get_objects(self, build_recursive_traversal_spec):
|
def test_get_objects(self, build_recursive_traversal_spec):
|
||||||
vim = mock.Mock()
|
vim = mock.Mock()
|
||||||
trav_spec = mock.Mock()
|
trav_spec = mock.Mock()
|
||||||
@ -215,7 +215,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
ret = vim_util.get_object_properties(vim, None, None)
|
ret = vim_util.get_object_properties(vim, None, None)
|
||||||
self.assertIsNone(ret)
|
self.assertIsNone(ret)
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.vim_util.cancel_retrieval')
|
@mock.patch('oslo_vmware.vim_util.cancel_retrieval')
|
||||||
def test_get_object_properties(self, cancel_retrieval):
|
def test_get_object_properties(self, cancel_retrieval):
|
||||||
vim = mock.Mock()
|
vim = mock.Mock()
|
||||||
moref = mock.Mock()
|
moref = mock.Mock()
|
||||||
@ -253,11 +253,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
self.assertTrue(res is retrieve_result.objects)
|
self.assertTrue(res is retrieve_result.objects)
|
||||||
cancel_retrieval.assert_called_once_with(vim, retrieve_result)
|
cancel_retrieval.assert_called_once_with(vim, retrieve_result)
|
||||||
|
|
||||||
def test_get_token(self):
|
@mock.patch('oslo_vmware.vim_util._get_token')
|
||||||
retrieve_result = object()
|
|
||||||
self.assertFalse(vim_util._get_token(retrieve_result))
|
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.vim_util._get_token')
|
|
||||||
def test_cancel_retrieval(self, get_token):
|
def test_cancel_retrieval(self, get_token):
|
||||||
token = mock.Mock()
|
token = mock.Mock()
|
||||||
get_token.return_value = token
|
get_token.return_value = token
|
||||||
@ -268,7 +264,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
vim.CancelRetrievePropertiesEx.assert_called_once_with(
|
vim.CancelRetrievePropertiesEx.assert_called_once_with(
|
||||||
vim.service_content.propertyCollector, token=token)
|
vim.service_content.propertyCollector, token=token)
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.vim_util._get_token')
|
@mock.patch('oslo_vmware.vim_util._get_token')
|
||||||
def test_continue_retrieval(self, get_token):
|
def test_continue_retrieval(self, get_token):
|
||||||
token = mock.Mock()
|
token = mock.Mock()
|
||||||
get_token.return_value = token
|
get_token.return_value = token
|
||||||
@ -279,7 +275,7 @@ class VimUtilTest(base.TestCase):
|
|||||||
vim.ContinueRetrievePropertiesEx.assert_called_once_with(
|
vim.ContinueRetrievePropertiesEx.assert_called_once_with(
|
||||||
vim.service_content.propertyCollector, token=token)
|
vim.service_content.propertyCollector, token=token)
|
||||||
|
|
||||||
@mock.patch('oslo.vmware.vim_util.get_object_properties')
|
@mock.patch('oslo_vmware.vim_util.get_object_properties')
|
||||||
def test_get_object_property(self, get_object_properties):
|
def test_get_object_property(self, get_object_properties):
|
||||||
prop = mock.Mock()
|
prop = mock.Mock()
|
||||||
prop.val = "ubuntu-12.04"
|
prop.val = "ubuntu-12.04"
|
||||||
|
3
tox.ini
3
tox.ini
@ -38,5 +38,6 @@ ignore = H405,H904
|
|||||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,__init__.py
|
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,__init__.py
|
||||||
|
|
||||||
[hacking]
|
[hacking]
|
||||||
import_exceptions = oslo.vmware._i18n
|
import_exceptions = oslo_vmware._i18n
|
||||||
|
oslo_vmware.tests.base
|
||||||
tests.base
|
tests.base
|
||||||
|
Loading…
Reference in New Issue
Block a user