VMware Datastore storage backend
Customers using a VMware environment with OpenStack should be able to store their Glance images in VMware datastores. This is a first step to solve the problem where Nova needs to copy the bits over the network from Glance to the datastore when spawning an instance. Also, this give the ability to provide some optimizations for specific image formats in the future (fast cloning for example). This patch contains a 'glance/store/vmware/' folder with the code to manage the connection with vCenter or an ESX(i) host. This code will go away as soon as it is merged to Olso: see review https://review.openstack.org/#/c/65075/ The current implementation give this ability to specify the vCenter or ESX(i) IP. In case of a vCenter IP, there is no optimization to reduce the datapath (no host selected). Consequently, it is recommended to specify an ESX IP if the ESX host API endpoint is accessible from Glance. docImpact Implements bp vmware-datastore-storage-backend Change-Id: I3837912e0d1614b9c31a689f71c2e34d453e2dc3
This commit is contained in:
parent
4d8c3f1553
commit
f9589bd010
@ -367,7 +367,8 @@ Optional. Default: ``file``
|
||||
Can only be specified in configuration files.
|
||||
|
||||
Sets the storage backend to use by default when storing images in Glance.
|
||||
Available options for this option are (``file``, ``swift``, ``s3``, ``rbd``, or ``sheepdog``, or ``cinder``).
|
||||
Available options for this option are (``file``, ``swift``, ``s3``, ``rbd``, ``sheepdog``,
|
||||
``cinder`` or ``vsphere``).
|
||||
|
||||
Configuring Glance Image Size Limit
|
||||
-----------------------------------
|
||||
|
@ -405,7 +405,7 @@ The list of metadata headers that Glance accepts are listed below.
|
||||
* ``x-image-meta-store``
|
||||
|
||||
This header is optional. Valid values are one of ``file``, ``s3``, ``rbd``,
|
||||
``swift``, ``cinder``, ``gridfs`` or ``sheepdog``
|
||||
``swift``, ``cinder``, ``gridfs``, ``sheepdog`` or ``vsphere``
|
||||
|
||||
When present, Glance will attempt to store the disk image data in the
|
||||
backing store indicated by the value of the header. If the Glance node
|
||||
|
@ -20,6 +20,7 @@ default_store = file
|
||||
# glance.store.swift.Store,
|
||||
# glance.store.sheepdog.Store,
|
||||
# glance.store.cinder.Store,
|
||||
# glance.store.vmware_datastore.Store,
|
||||
|
||||
|
||||
# Maximum image size (in bytes) that may be uploaded through the
|
||||
@ -457,6 +458,42 @@ sheepdog_store_chunk_size = 64
|
||||
# Allow to perform insecure SSL requests to cinder (boolean value)
|
||||
#cinder_api_insecure = False
|
||||
|
||||
# ============ VMware Datastore Store Options =====================
|
||||
|
||||
# ESX/ESXi or vCenter Server target system.
|
||||
# The server value can be an IP address or a DNS name
|
||||
# e.g. 127.0.0.1, 127.0.0.1:443, www.vmware-infra.com
|
||||
#vmware_server_host = <None>
|
||||
|
||||
# Server username (string value)
|
||||
#vmware_server_username = <None>
|
||||
|
||||
# Server password (string value)
|
||||
#vmware_server_password = <None>
|
||||
|
||||
# Inventory path to a datacenter (string value)
|
||||
# Value optional when vmware_server_ip is an ESX/ESXi host: if specified
|
||||
# should be `ha-datacenter`.
|
||||
#vmware_datacenter_path = <None>
|
||||
|
||||
# Datastore associated with the datacenter (string value)
|
||||
#vmware_datastore_name = <None>
|
||||
|
||||
# The number of times we retry on failures
|
||||
# e.g., socket error, etc (integer value)
|
||||
#vmware_api_retry_count = 10
|
||||
|
||||
# The interval used for polling remote tasks
|
||||
# invoked on VMware ESX/VC server in seconds (integer value)
|
||||
#vmware_task_poll_interval = 5
|
||||
|
||||
# Absolute path of the folder containing the images in the datastore
|
||||
# (string value)
|
||||
#vmware_store_image_dir = /openstack_glance
|
||||
|
||||
# Allow to perform insecure SSL requests to the target system (boolean value)
|
||||
#vmware_api_insecure = False
|
||||
|
||||
# ============ Delayed Delete Options =============================
|
||||
|
||||
# Turn on/off delayed delete
|
||||
|
@ -46,6 +46,7 @@ registry_port = 9191
|
||||
# glance.store.swift.Store,
|
||||
# glance.store.sheepdog.Store,
|
||||
# glance.store.cinder.Store,
|
||||
# glance.store.vmware_datastore.Store,
|
||||
|
||||
# ============ Filesystem Store Options ========================
|
||||
|
||||
@ -155,6 +156,42 @@ s3_store_create_bucket_on_put = False
|
||||
# Allow to perform insecure SSL requests to cinder (boolean value)
|
||||
#cinder_api_insecure = False
|
||||
|
||||
# ============ VMware Datastore Store Options =====================
|
||||
|
||||
# ESX/ESXi or vCenter Server target system.
|
||||
# The server value can be an IP address or a DNS name
|
||||
# e.g. 127.0.0.1, 127.0.0.1:443, www.vmware-infra.com
|
||||
#vmware_server_host = <None>
|
||||
|
||||
# Server username (string value)
|
||||
#vmware_server_username = <None>
|
||||
|
||||
# Server password (string value)
|
||||
#vmware_server_password = <None>
|
||||
|
||||
# Inventory path to a datacenter (string value)
|
||||
# Value optional when vmware_server_ip is an ESX/ESXi host: if specified
|
||||
# should be `ha-datacenter`.
|
||||
#vmware_datacenter_path = <None>
|
||||
|
||||
# Datastore associated with the datacenter (string value)
|
||||
#vmware_datastore_name = <None>
|
||||
|
||||
# The number of times we retry on failures
|
||||
# e.g., socket error, etc (integer value)
|
||||
#vmware_api_retry_count = 10
|
||||
|
||||
# The interval used for polling remote tasks
|
||||
# invoked on VMware ESX/VC server in seconds (integer value)
|
||||
#vmware_task_poll_interval = 5
|
||||
|
||||
# Absolute path of the folder containing the images in the datastore
|
||||
# (string value)
|
||||
#vmware_store_image_dir = /openstack_glance
|
||||
|
||||
# Allow to perform insecure SSL requests to the target system (boolean value)
|
||||
#vmware_api_insecure = False
|
||||
|
||||
# ================= Security Options ==========================
|
||||
|
||||
# AES key for encrypting store 'location' metadata, including
|
||||
|
@ -39,6 +39,7 @@ store_opts = [
|
||||
'glance.store.swift.Store',
|
||||
'glance.store.sheepdog.Store',
|
||||
'glance.store.cinder.Store',
|
||||
'glance.store.vmware_datastore.Store',
|
||||
],
|
||||
help=_('List of which store classes and store class locations '
|
||||
'are currently known to glance at startup.')),
|
||||
|
@ -65,6 +65,7 @@ def get_location_from_uri(uri):
|
||||
s3+https://accesskey:secretkey@s3.amazonaws.com/bucket/key-id
|
||||
file:///var/lib/glance/images/1
|
||||
cinder://volume-id
|
||||
vsphere://server_host/folder/file_path?dcPath=dc_path&dsName=ds_name
|
||||
"""
|
||||
pieces = urlparse.urlparse(uri)
|
||||
if pieces.scheme not in SCHEME_TO_CLS_MAP.keys():
|
||||
|
0
glance/store/vmware/__init__.py
Normal file
0
glance/store/vmware/__init__.py
Normal file
273
glance/store/vmware/api.py
Normal file
273
glance/store/vmware/api.py
Normal file
@ -0,0 +1,273 @@
|
||||
# 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.
|
||||
Provides abstraction over glance.vmware.vim.Vim SOAP calls.
|
||||
"""
|
||||
|
||||
from eventlet import event
|
||||
|
||||
import glance.openstack.common.log as logging
|
||||
from glance.openstack.common import loopingcall
|
||||
from glance.store.vmware import error_util
|
||||
from glance.store.vmware import vim
|
||||
from glance.store.vmware import vim_util
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Retry(object):
|
||||
"""Decorator for retrying a function upon suggested exceptions.
|
||||
|
||||
The method retries for given number of times and the sleep
|
||||
time increments till the max sleep time is reached.
|
||||
If max retries is set to -1, then the decorated function is
|
||||
invoked indefinitely till no exception is thrown or if
|
||||
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=()):
|
||||
"""Initialize retry object based on input params.
|
||||
|
||||
:param max_retry_count: Max number of times, a function must be
|
||||
retried when one of input 'exceptions'
|
||||
is caught. The default -1 will always
|
||||
retry the function till a non-exception
|
||||
case, or an un-wanted error case arises.
|
||||
:param inc_sleep_time: Incremental time in seconds for sleep time
|
||||
between retrial
|
||||
:param max_sleep_time: Max sleep time beyond which the sleep time will
|
||||
not be incremented using param inc_sleep_time
|
||||
and max_sleep_time will be used as 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(done, *args, **kwargs):
|
||||
try:
|
||||
result = f(*args, **kwargs)
|
||||
done.send(result)
|
||||
except self._exceptions as excep:
|
||||
LOG.exception(_("Failure while invoking function: "
|
||||
"%(func)s. Error: %(excep)s.") %
|
||||
{'func': f.__name__, 'excep': excep})
|
||||
if (self._max_retry_count != -1 and
|
||||
self._retry_count >= self._max_retry_count):
|
||||
done.send_exception(excep)
|
||||
else:
|
||||
self._retry_count += 1
|
||||
self._sleep_time += self._inc_sleep_time
|
||||
return self._sleep_time
|
||||
except Exception as excep:
|
||||
done.send_exception(excep)
|
||||
return 0
|
||||
|
||||
def func(*args, **kwargs):
|
||||
done = event.Event()
|
||||
loop = loopingcall.DynamicLoopingCall(_func, done, *args, **kwargs)
|
||||
loop.start(periodic_interval_max=self._max_sleep_time)
|
||||
result = done.wait()
|
||||
loop.stop()
|
||||
return result
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class VMwareAPISession(object):
|
||||
"""Sets up a session with the server and handles all calls made to it."""
|
||||
|
||||
def __init__(self, server_ip, server_username, server_password,
|
||||
api_retry_count, task_poll_interval=5.0,
|
||||
scheme='https', create_session=True,
|
||||
wsdl_loc=None):
|
||||
"""Constructs session object.
|
||||
|
||||
:param server_ip: IP address of ESX/VC server
|
||||
: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 scheme: http or https protocol
|
||||
:param create_session: Boolean whether to set up connection at the
|
||||
time of instance creation
|
||||
:param wsdl_loc: WSDL file location for invoking SOAP calls on server
|
||||
using suds
|
||||
"""
|
||||
self._server_ip = server_ip
|
||||
self._server_username = server_username
|
||||
self._server_password = server_password
|
||||
self._wsdl_loc = wsdl_loc
|
||||
self._api_retry_count = api_retry_count
|
||||
self._task_poll_interval = task_poll_interval
|
||||
self._scheme = scheme
|
||||
self._session_id = None
|
||||
self._vim = None
|
||||
if create_session:
|
||||
self.create_session()
|
||||
|
||||
@property
|
||||
def vim(self):
|
||||
if not self._vim:
|
||||
self._vim = vim.Vim(protocol=self._scheme, host=self._server_ip,
|
||||
wsdl_loc=self._wsdl_loc)
|
||||
return self._vim
|
||||
|
||||
@Retry(exceptions=(Exception))
|
||||
def create_session(self):
|
||||
"""Establish session with the server."""
|
||||
# Login and setup the session with the server for making
|
||||
# API calls
|
||||
session_manager = self.vim.service_content.sessionManager
|
||||
session = self.vim.Login(session_manager,
|
||||
userName=self._server_username,
|
||||
password=self._server_password)
|
||||
# Terminate the earlier session, if possible (For the sake of
|
||||
# preserving sessions as there is a limit to the number of
|
||||
# sessions we can have)
|
||||
if self._session_id:
|
||||
try:
|
||||
self.vim.TerminateSession(session_manager,
|
||||
sessionId=[self._session_id])
|
||||
except Exception as excep:
|
||||
# This exception is something we can live with. It is
|
||||
# just an extra caution on our side. The session may
|
||||
# have been cleared. We could have made a call to
|
||||
# SessionIsActive, but that is an overhead because we
|
||||
# anyway would have to call TerminateSession.
|
||||
LOG.exception(_("Error while terminating session: %s.") %
|
||||
excep)
|
||||
self._session_id = session.key
|
||||
LOG.info(_("Successfully established connection to the server."))
|
||||
|
||||
def __del__(self):
|
||||
"""Logs-out the session."""
|
||||
try:
|
||||
self.vim.Logout(self.vim.service_content.sessionManager)
|
||||
except Exception as excep:
|
||||
LOG.exception(_("Error while logging out the user: %s.") %
|
||||
excep)
|
||||
|
||||
def invoke_api(self, module, method, *args, **kwargs):
|
||||
"""Wrapper method for invoking APIs.
|
||||
|
||||
Here we retry the API calls for exceptions which may come because
|
||||
of session overload.
|
||||
|
||||
Make sure if a Vim instance is being passed here, this session's
|
||||
Vim (self.vim) instance is used, as we retry establishing session
|
||||
in case of session timedout.
|
||||
|
||||
:param module: Module invoking the VI SDK calls
|
||||
:param method: Method in the module that invokes the VI SDK call
|
||||
:param args: Arguments to the method
|
||||
:param kwargs: Keyword arguments to the method
|
||||
:return: Response of the API call
|
||||
"""
|
||||
|
||||
@Retry(max_retry_count=self._api_retry_count,
|
||||
exceptions=(error_util.VimException))
|
||||
def _invoke_api(module, method, *args, **kwargs):
|
||||
last_fault_list = []
|
||||
while True:
|
||||
try:
|
||||
api_method = getattr(module, method)
|
||||
return api_method(*args, **kwargs)
|
||||
except error_util.VimFaultException as excep:
|
||||
if error_util.NOT_AUTHENTICATED not in excep.fault_list:
|
||||
raise excep
|
||||
# If it is a not-authenticated fault, we re-authenticate
|
||||
# the user and retry the API invocation.
|
||||
|
||||
# Because of the idle session returning an empty
|
||||
# RetrieveProperties response and also the same is
|
||||
# returned when there is an empty answer to a query
|
||||
# (e.g. no VMs on the host), we have no way to
|
||||
# differentiate.
|
||||
# So if the previous response was also an empty
|
||||
# response and after creating a new session, we get
|
||||
# the same empty response, then we are sure of the
|
||||
# response being an empty response.
|
||||
if error_util.NOT_AUTHENTICATED in last_fault_list:
|
||||
return []
|
||||
last_fault_list = excep.fault_list
|
||||
LOG.warn(_("Not authenticated error occurred. "
|
||||
"Will create session and try "
|
||||
"API call again: %s.") % excep)
|
||||
self.create_session()
|
||||
|
||||
return _invoke_api(module, method, *args, **kwargs)
|
||||
|
||||
def _stop_loop(self, loop):
|
||||
loop.stop()
|
||||
|
||||
def wait_for_task(self, task):
|
||||
"""Return a deferred that will give the result of the given task.
|
||||
|
||||
The task is polled until it completes. The method returns the task
|
||||
information upon successful completion.
|
||||
|
||||
:param task: Managed object reference of the task
|
||||
:return: Task info upon successful completion of the task
|
||||
"""
|
||||
done = event.Event()
|
||||
loop = loopingcall.FixedIntervalLoopingCall(self._poll_task,
|
||||
task, done)
|
||||
loop.start(self._task_poll_interval)
|
||||
task_info = done.wait()
|
||||
loop.stop()
|
||||
return task_info
|
||||
|
||||
def _poll_task(self, task, done):
|
||||
"""Poll the given task.
|
||||
|
||||
If the task completes successfully then returns task info.
|
||||
In case of error sends back appropriate error.
|
||||
|
||||
:param task: Managed object reference of the task
|
||||
:param done: Event that captures task status
|
||||
"""
|
||||
try:
|
||||
task_info = self.invoke_api(vim_util, 'get_object_property',
|
||||
self.vim, task, 'info')
|
||||
if task_info.state in ['queued', 'running']:
|
||||
# If task already completed on server, it will not return
|
||||
# the progress.
|
||||
if hasattr(task_info, 'progress'):
|
||||
LOG.debug(_("Task: %(task)s progress: %(prog)s.") %
|
||||
{'task': task, 'prog': task_info.progress})
|
||||
return
|
||||
elif task_info.state == 'success':
|
||||
LOG.debug(_("Task %s status: success.") % task)
|
||||
done.send(task_info)
|
||||
else:
|
||||
error_msg = str(task_info.error.localizedMessage)
|
||||
LOG.exception(_("Task: %(task)s failed with error: %(err)s.") %
|
||||
{'task': task, 'err': error_msg})
|
||||
done.send_exception(error_util.VimFaultException([],
|
||||
error_msg))
|
||||
except Exception as excep:
|
||||
LOG.exception(_("Task: %(task)s failed with error: %(err)s.") %
|
||||
{'task': task, 'err': excep})
|
||||
done.send_exception(excep)
|
48
glance/store/vmware/error_util.py
Normal file
48
glance/store/vmware/error_util.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 classes and SOAP response error checking module.
|
||||
"""
|
||||
|
||||
from glance.common import exception
|
||||
|
||||
|
||||
NOT_AUTHENTICATED = 'NotAuthenticated'
|
||||
|
||||
|
||||
class VimException(exception.GlanceException):
|
||||
"""The VIM Exception class."""
|
||||
|
||||
def __init__(self, msg):
|
||||
exception.GlanceException.__init__(self, msg)
|
||||
|
||||
|
||||
class SessionOverLoadException(VimException):
|
||||
"""Session Overload Exception."""
|
||||
pass
|
||||
|
||||
|
||||
class VimAttributeException(VimException):
|
||||
"""VI Attribute Error."""
|
||||
pass
|
||||
|
||||
|
||||
class VimFaultException(VimException):
|
||||
"""The VIM Fault exception class."""
|
||||
|
||||
def __init__(self, fault_list, msg):
|
||||
super(VimFaultException, self).__init__(msg)
|
||||
self.fault_list = fault_list
|
241
glance/store/vmware/vim.py
Normal file
241
glance/store/vmware/vim.py
Normal file
@ -0,0 +1,241 @@
|
||||
# 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 for making VMware VI SOAP calls.
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import logging
|
||||
|
||||
import suds
|
||||
|
||||
from glance.store.vmware import error_util
|
||||
|
||||
logging.getLogger('suds').setLevel(logging.INFO)
|
||||
|
||||
RESP_NOT_XML_ERROR = "Response is 'text/html', not 'text/xml'"
|
||||
CONN_ABORT_ERROR = 'Software caused connection abort'
|
||||
ADDRESS_IN_USE_ERROR = 'Address already in use'
|
||||
|
||||
|
||||
def get_moref(value, type):
|
||||
"""Get managed object reference.
|
||||
|
||||
:param value: value for the managed object
|
||||
:param type: type of the managed object
|
||||
:return: Managed object reference with with input value and type
|
||||
"""
|
||||
moref = suds.sudsobject.Property(value)
|
||||
moref._type = type
|
||||
return moref
|
||||
|
||||
|
||||
class VIMMessagePlugin(suds.plugin.MessagePlugin):
|
||||
|
||||
def addAttributeForValue(self, node):
|
||||
"""Helper to handle AnyType.
|
||||
|
||||
suds does not handle AnyType properly.
|
||||
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):
|
||||
"""Marshal soap context.
|
||||
|
||||
Provides the plugin with the opportunity to prune empty
|
||||
nodes and fixup nodes before sending it to the server.
|
||||
|
||||
:param context: SOAP 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.addAttributeForValue)
|
||||
|
||||
|
||||
class Vim(object):
|
||||
"""The VIM Object."""
|
||||
|
||||
def __init__(self, protocol='https', host='localhost', wsdl_loc=None):
|
||||
"""Create communication interfaces for initiating SOAP transactions.
|
||||
|
||||
:param protocol: http or https
|
||||
:param host: Server IPAddress[:port] or Hostname[:port]
|
||||
:param wsdl_loc: Optional location of the VIM WSDL
|
||||
"""
|
||||
self._protocol = protocol
|
||||
self._host_name = host
|
||||
if not wsdl_loc:
|
||||
wsdl_loc = Vim._get_wsdl_loc(protocol, host)
|
||||
soap_url = Vim._get_soap_url(protocol, host)
|
||||
self._client = suds.client.Client(wsdl_loc, location=soap_url,
|
||||
plugins=[VIMMessagePlugin()])
|
||||
self._service_content = self.RetrieveServiceContent('ServiceInstance')
|
||||
|
||||
@staticmethod
|
||||
def _get_wsdl_loc(protocol, host_name):
|
||||
"""Return default WSDL file location hosted at the server.
|
||||
|
||||
:param protocol: http or https
|
||||
:param host_name: ESX/VC server host name
|
||||
:return: Default WSDL file location hosted at the server
|
||||
"""
|
||||
return '%s://%s/sdk/vimService.wsdl' % (protocol, host_name)
|
||||
|
||||
@staticmethod
|
||||
def _get_soap_url(protocol, host_name):
|
||||
"""Return URL to SOAP services for ESX/VC server.
|
||||
|
||||
:param protocol: https or http
|
||||
:param host_name: ESX/VC server host name
|
||||
:return: URL to SOAP services for ESX/VC server
|
||||
"""
|
||||
return '%s://%s/sdk' % (protocol, host_name)
|
||||
|
||||
@property
|
||||
def service_content(self):
|
||||
return self._service_content
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self._client
|
||||
|
||||
def __getattr__(self, attr_name):
|
||||
"""Makes the API call and gets the result."""
|
||||
|
||||
def retrieve_properties_ex_fault_checker(response):
|
||||
"""Checks the RetrievePropertiesEx response for errors.
|
||||
|
||||
Certain faults are sent as part of the SOAP body as property of
|
||||
missingSet. For example NotAuthenticated fault. The method raises
|
||||
appropriate VimFaultException when an error is found.
|
||||
|
||||
:param response: Response from RetrievePropertiesEx API call
|
||||
"""
|
||||
|
||||
fault_list = []
|
||||
if not response:
|
||||
# This is the case when the session has timed out. ESX SOAP
|
||||
# server sends an empty RetrievePropertiesExResponse. Normally
|
||||
# missingSet in the returnval field 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. So setting fault to NotAuthenticated fault.
|
||||
fault_list = [error_util.NOT_AUTHENTICATED]
|
||||
else:
|
||||
for obj_cont in response:
|
||||
if hasattr(obj_cont, 'missingSet'):
|
||||
for missing_elem in obj_cont.missingSet:
|
||||
fault_type = missing_elem.fault.fault.__class__
|
||||
# Fault needs to be added to the type of fault
|
||||
# for uniformity in error checking as SOAP faults
|
||||
# define
|
||||
fault_list.append(fault_type.__name__)
|
||||
if fault_list:
|
||||
exc_msg_list = ', '.join(fault_list)
|
||||
raise error_util.VimFaultException(fault_list,
|
||||
_("Error(s): %s occurred "
|
||||
"in the call to "
|
||||
"RetrievePropertiesEx.") %
|
||||
exc_msg_list)
|
||||
|
||||
def vim_request_handler(managed_object, **kwargs):
|
||||
"""Handler for VI SDK calls.
|
||||
|
||||
Builds the SOAP message and parses the response for fault
|
||||
checking and other errors.
|
||||
|
||||
:param managed_object:Managed object reference
|
||||
:param kwargs: Keyword arguments of the call
|
||||
:return: Response of the API call
|
||||
"""
|
||||
|
||||
try:
|
||||
if isinstance(managed_object, str):
|
||||
# For strings use string value for value and type
|
||||
# of the managed object.
|
||||
managed_object = get_moref(managed_object, managed_object)
|
||||
request = getattr(self.client.service, attr_name)
|
||||
response = request(managed_object, **kwargs)
|
||||
if (attr_name.lower() == 'retrievepropertiesex'):
|
||||
retrieve_properties_ex_fault_checker(response)
|
||||
return response
|
||||
|
||||
except error_util.VimFaultException as excep:
|
||||
raise
|
||||
|
||||
except suds.WebFault as excep:
|
||||
doc = excep.document
|
||||
detail = doc.childAtPath('/Envelope/Body/Fault/detail')
|
||||
fault_list = []
|
||||
for child in detail.getChildren():
|
||||
fault_list.append(child.get('type'))
|
||||
raise error_util.VimFaultException(fault_list, str(excep))
|
||||
|
||||
except AttributeError as excep:
|
||||
raise error_util.VimAttributeException(_("No such SOAP method "
|
||||
"%(attr)s. Detailed "
|
||||
"error: %(excep)s.") %
|
||||
{'attr': attr_name,
|
||||
'excep': excep})
|
||||
|
||||
except (httplib.CannotSendRequest,
|
||||
httplib.ResponseNotReady,
|
||||
httplib.CannotSendHeader) as excep:
|
||||
raise error_util.SessionOverLoadException(_("httplib error in "
|
||||
"%(attr)s: "
|
||||
"%(excep)s.") %
|
||||
{'attr': attr_name,
|
||||
'excep': excep})
|
||||
|
||||
except Exception as excep:
|
||||
# Socket errors which need special handling for they
|
||||
# might be caused by server API call overload
|
||||
if (str(excep).find(ADDRESS_IN_USE_ERROR) != -1 or
|
||||
str(excep).find(CONN_ABORT_ERROR)) != -1:
|
||||
raise error_util.SessionOverLoadException(_("Socket error "
|
||||
"in %(attr)s: "
|
||||
"%(excep)s.") %
|
||||
{'attr':
|
||||
attr_name,
|
||||
'excep': excep})
|
||||
# Type error that needs special handling for it might be
|
||||
# caused by server API call overload
|
||||
elif str(excep).find(RESP_NOT_XML_ERROR) != -1:
|
||||
raise error_util.SessionOverLoadException(_("Type error "
|
||||
"in %(attr)s: "
|
||||
"%(excep)s.") %
|
||||
{'attr':
|
||||
attr_name,
|
||||
'excep': excep})
|
||||
else:
|
||||
raise error_util.VimException(_("Error in %(attr)s. "
|
||||
"Detailed error: "
|
||||
"%(excep)s.") %
|
||||
{'attr': attr_name,
|
||||
'excep': excep})
|
||||
return vim_request_handler
|
||||
|
||||
def __repr__(self):
|
||||
return "VIM Object."
|
||||
|
||||
def __str__(self):
|
||||
return "VIM Object."
|
301
glance/store/vmware/vim_util.py
Normal file
301
glance/store/vmware/vim_util.py
Normal file
@ -0,0 +1,301 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
:return: 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 object.
|
||||
|
||||
:param client_factory: Factory to get API input specs
|
||||
:param name: Name for the traversal spec
|
||||
:param type: Type of the managed object reference
|
||||
:param path: Property path of the managed object reference
|
||||
: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
|
||||
:return: 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
|
||||
:return: 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])
|
||||
|
||||
# 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,
|
||||
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 reference property
|
||||
:param properties_to_collect: Properties of the managed object reference
|
||||
to be collected while traversal filtering
|
||||
:param all_properties: Whether all the properties of managed object
|
||||
reference needs to be collected
|
||||
:return: 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 as the starting point for
|
||||
traversal
|
||||
:param traversal_specs: filter specs required for traversal
|
||||
:return: 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
|
||||
:return: 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, props_to_collect=None,
|
||||
all_properties=False):
|
||||
"""Gets all managed object references of a specified type.
|
||||
|
||||
It is caller's responsibility to continue or cancel retrieval.
|
||||
|
||||
:param vim: Vim object
|
||||
:param type: Type of the managed object reference
|
||||
:param max_objects: Maximum number of objects that should be returned in
|
||||
a single call
|
||||
:param props_to_collect: Properties of the managed object reference
|
||||
to be collected
|
||||
:param all_properties: Whether all properties of the managed object
|
||||
reference are to be collected
|
||||
:return: All managed object references of a specified type
|
||||
"""
|
||||
|
||||
if not props_to_collect:
|
||||
props_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=props_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, mobj, properties):
|
||||
"""Gets properties of the managed object specified.
|
||||
|
||||
:param vim: Vim object
|
||||
:param mobj: Reference to the managed object
|
||||
:param properties: Properties of the managed object reference
|
||||
to be retrieved
|
||||
:return: Properties of the managed object specified
|
||||
"""
|
||||
|
||||
client_factory = vim.client.factory
|
||||
if mobj is None:
|
||||
return None
|
||||
collector = vim.service_content.propertyCollector
|
||||
property_filter_spec = client_factory.create('ns0:PropertyFilterSpec')
|
||||
property_spec = client_factory.create('ns0:PropertySpec')
|
||||
property_spec.all = (properties is None or len(properties) == 0)
|
||||
property_spec.pathSet = properties
|
||||
property_spec.type = mobj._type
|
||||
object_spec = client_factory.create('ns0:ObjectSpec')
|
||||
object_spec.obj = mobj
|
||||
object_spec.skip = False
|
||||
property_filter_spec.propSet = [property_spec]
|
||||
property_filter_spec.objectSet = [object_spec]
|
||||
options = client_factory.create('ns0:RetrieveOptions')
|
||||
options.maxObjects = 1
|
||||
retrieve_result = vim.RetrievePropertiesEx(collector,
|
||||
specSet=[property_filter_spec],
|
||||
options=options)
|
||||
cancel_retrieval(vim, retrieve_result)
|
||||
return retrieve_result.objects
|
||||
|
||||
|
||||
def _get_token(retrieve_result):
|
||||
"""Get token from results to obtain next set of results.
|
||||
|
||||
:retrieve_result: Result from the RetrievePropertiesEx API
|
||||
:return: 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 from the RetrievePropertiesEx API
|
||||
"""
|
||||
|
||||
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 present.
|
||||
|
||||
:param vim: Vim object
|
||||
:param retrieve_result: Result from the RetrievePropertiesEx API
|
||||
"""
|
||||
|
||||
token = _get_token(retrieve_result)
|
||||
if token:
|
||||
collector = vim.service_content.propertyCollector
|
||||
return vim.ContinueRetrievePropertiesEx(collector, token=token)
|
||||
|
||||
|
||||
def get_object_property(vim, mobj, property_name):
|
||||
"""Gets property of the managed object specified.
|
||||
|
||||
:param vim: Vim object
|
||||
:param mobj: Reference to the managed object
|
||||
:param property_name: Name of the property to be retrieved
|
||||
:return: Property of the managed object specified
|
||||
"""
|
||||
props = get_object_properties(vim, mobj, [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
|
372
glance/store/vmware_datastore.py
Normal file
372
glance/store/vmware_datastore.py
Normal file
@ -0,0 +1,372 @@
|
||||
# Copyright 2014 OpenStack, LLC
|
||||
# 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.
|
||||
|
||||
"""Storage backend for VMware Datastore"""
|
||||
|
||||
import hashlib
|
||||
import httplib
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
import netaddr
|
||||
from oslo.config import cfg
|
||||
|
||||
from glance.common import exception
|
||||
import glance.openstack.common.log as logging
|
||||
import glance.store
|
||||
import glance.store.base
|
||||
import glance.store.location
|
||||
from glance.store.vmware import api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MAX_REDIRECTS = 5
|
||||
DEFAULT_STORE_IMAGE_DIR = '/openstack_glance'
|
||||
DEFAULT_ESX_DATACENTER_PATH = 'ha-datacenter'
|
||||
DS_URL_PREFIX = '/folder'
|
||||
|
||||
# check that datacenter/datastore combination is valid
|
||||
_datastore_info_valid = False
|
||||
|
||||
vmware_opts = [
|
||||
cfg.StrOpt('vmware_server_host',
|
||||
help=_('ESX/ESXi or vCenter Server target system. '
|
||||
'The server value can be an IP address or a DNS name.')),
|
||||
cfg.StrOpt('vmware_server_username',
|
||||
help=_('Username for authenticating with '
|
||||
'VMware ESX/VC server.')),
|
||||
cfg.StrOpt('vmware_server_password',
|
||||
help=_('Password for authenticating with '
|
||||
'VMware ESX/VC server.'),
|
||||
secret=True),
|
||||
cfg.StrOpt('vmware_datacenter_path',
|
||||
default=DEFAULT_ESX_DATACENTER_PATH,
|
||||
help=_('Inventory path to a datacenter. '
|
||||
'If the vmware_server_host specified is an ESX/ESXi, '
|
||||
'the vmware_datacenter_path is optional. If specified, '
|
||||
'it should be "ha-datacenter".')),
|
||||
cfg.StrOpt('vmware_datastore_name',
|
||||
help=_('Datastore associated with the datacenter.')),
|
||||
cfg.IntOpt('vmware_api_retry_count',
|
||||
default=10,
|
||||
help=_('Number of times VMware ESX/VC server API must be '
|
||||
'retried upon connection related issues.')),
|
||||
cfg.IntOpt('vmware_task_poll_interval',
|
||||
default=5,
|
||||
help=_('The interval used for polling remote tasks '
|
||||
'invoked on VMware ESX/VC server.')),
|
||||
cfg.StrOpt('vmware_store_image_dir',
|
||||
default=DEFAULT_STORE_IMAGE_DIR,
|
||||
help=_('The name of the directory where the glance images '
|
||||
'will be stored in the VMware datastore.')),
|
||||
cfg.BoolOpt('vmware_api_insecure',
|
||||
default=False,
|
||||
help=_('Allow to perform insecure SSL requests to ESX/VC')),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(vmware_opts)
|
||||
|
||||
|
||||
def is_valid_ipv6(address):
|
||||
try:
|
||||
return netaddr.valid_ipv6(address)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def http_response_iterator(conn, response, size):
|
||||
"""Return an iterator for a file-like object.
|
||||
|
||||
:param conn: HTTP(S) Connection
|
||||
:param response: httplib.HTTPResponse object
|
||||
:param size: Chunk size to iterate with
|
||||
"""
|
||||
try:
|
||||
chunk = response.read(size)
|
||||
while chunk:
|
||||
yield chunk
|
||||
chunk = response.read(size)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class _Reader(object):
|
||||
|
||||
def __init__(self, data, checksum):
|
||||
self.data = data
|
||||
self.checksum = checksum
|
||||
|
||||
def read(self, len):
|
||||
result = self.data.read(len)
|
||||
self.checksum.update(result)
|
||||
return result
|
||||
|
||||
|
||||
class StoreLocation(glance.store.location.StoreLocation):
|
||||
"""Class describing an VMware URI.
|
||||
|
||||
An VMware URI can look like any of the following:
|
||||
vsphere://server_host/folder/file_path?dcPath=dc_path&dsName=ds_name
|
||||
"""
|
||||
|
||||
def process_specs(self):
|
||||
self.scheme = self.specs.get('scheme', 'vsphere')
|
||||
self.server_host = self.specs.get('server_host')
|
||||
self.path = (DS_URL_PREFIX + self.specs.get('folder_name')
|
||||
+ '/' + self.specs.get('image_id'))
|
||||
dc_path = self.specs.get('datacenter_path')
|
||||
if dc_path is not None:
|
||||
param_list = {'dcPath': self.specs.get('datacenter_path'),
|
||||
'dsName': self.specs.get('datastore_name')}
|
||||
else:
|
||||
param_list = {'dsName': self.specs.get('datastore_name')}
|
||||
self.query = urllib.urlencode(param_list)
|
||||
|
||||
def get_uri(self):
|
||||
if is_valid_ipv6(self.server_host):
|
||||
base_url = '%s://[%s]%s' % (self.scheme,
|
||||
self.server_host, self.path)
|
||||
else:
|
||||
base_url = '%s://%s%s' % (self.scheme,
|
||||
self.server_host, self.path)
|
||||
|
||||
return base_url + '?' + self.query
|
||||
|
||||
def _is_valid_path(self, path):
|
||||
return path.startswith(DS_URL_PREFIX + CONF.vmware_store_image_dir)
|
||||
|
||||
def parse_uri(self, uri):
|
||||
(self.scheme, self.server_host,
|
||||
path, params, query, fragment) = urlparse.urlparse(uri)
|
||||
if not query:
|
||||
path = path.split('?')
|
||||
if self._is_valid_path(path[0]):
|
||||
self.path = path[0]
|
||||
self.query = path[1]
|
||||
return
|
||||
elif self._is_valid_path(path):
|
||||
self.path = path
|
||||
self.query = query
|
||||
return
|
||||
reason = (_('Badly formed VMware datastore URI %(uri)s.')
|
||||
% {'uri': uri})
|
||||
LOG.debug(reason)
|
||||
raise exception.BadStoreUri(reason)
|
||||
|
||||
|
||||
class Store(glance.store.base.Store):
|
||||
"""An implementation of the VMware datastore adapter."""
|
||||
|
||||
def get_schemes(self):
|
||||
return ('vsphere',)
|
||||
|
||||
def configure(self):
|
||||
self.scheme = 'vsphere'
|
||||
self.server_host = self._option_get('vmware_server_host')
|
||||
self.server_username = self._option_get('vmware_server_username')
|
||||
self.server_password = self._option_get('vmware_server_password')
|
||||
self.api_retry_count = CONF.vmware_api_retry_count
|
||||
self.task_poll_interval = CONF.vmware_task_poll_interval
|
||||
self.api_insecure = CONF.vmware_api_insecure
|
||||
self._session = api.VMwareAPISession(self.server_host,
|
||||
self.server_username,
|
||||
self.server_password,
|
||||
self.api_retry_count,
|
||||
self.task_poll_interval)
|
||||
self._service_content = self._session.vim.service_content
|
||||
|
||||
def configure_add(self):
|
||||
self.datacenter_path = CONF.vmware_datacenter_path
|
||||
self.datastore_name = self._option_get('vmware_datastore_name')
|
||||
global _datastore_info_valid
|
||||
if not _datastore_info_valid:
|
||||
search_index_moref = self._service_content.searchIndex
|
||||
|
||||
inventory_path = ('%s/datastore/%s'
|
||||
% (self.datacenter_path, self.datastore_name))
|
||||
ds_moref = self._session.invoke_api(self._session.vim,
|
||||
'FindByInventoryPath',
|
||||
search_index_moref,
|
||||
inventoryPath=inventory_path)
|
||||
if ds_moref is None:
|
||||
reason = (_("Could not find datastore %(ds_name)s "
|
||||
"in datacenter %(dc_path)s")
|
||||
% {'ds_name': self.datastore_name,
|
||||
'dc_path': self.datacenter_path})
|
||||
raise exception.BadStoreConfiguration(
|
||||
store_name='vmware_datastore', reason=reason)
|
||||
else:
|
||||
ds_validated = True
|
||||
|
||||
self.store_image_dir = CONF.vmware_store_image_dir
|
||||
|
||||
def _option_get(self, param):
|
||||
result = getattr(CONF, param)
|
||||
if not result:
|
||||
reason = (_("Could not find %(param)s in configuration "
|
||||
"options.") % {'param': param})
|
||||
raise exception.BadStoreConfiguration(
|
||||
store_name='vmware_datastore', reason=reason)
|
||||
return result
|
||||
|
||||
def _build_vim_cookie_header(self, vim_cookies):
|
||||
"""Build ESX host session cookie header."""
|
||||
if len(list(vim_cookies)) > 0:
|
||||
cookie = list(vim_cookies)[0]
|
||||
return cookie.name + '=' + cookie.value
|
||||
|
||||
def add(self, image_id, image_file, image_size):
|
||||
"""Stores an image file with supplied identifier to the backend
|
||||
storage system and returns a tuple containing information
|
||||
about the stored image.
|
||||
|
||||
:param image_id: The opaque image identifier
|
||||
:param image_file: The image data to write, as a file-like object
|
||||
:param image_size: The size of the image data to write, in bytes
|
||||
:retval tuple of URL in backing store, bytes written, checksum
|
||||
and a dictionary with storage system specific information
|
||||
:raises `glance.common.exception.Duplicate` if the image already
|
||||
existed
|
||||
"""
|
||||
checksum = hashlib.md5()
|
||||
image_file = _Reader(image_file, checksum)
|
||||
loc = StoreLocation({'scheme': self.scheme,
|
||||
'server_host': self.server_host,
|
||||
'folder_name': self.store_image_dir,
|
||||
'datacenter_path': self.datacenter_path,
|
||||
'datastore_name': self.datastore_name,
|
||||
'image_id': image_id})
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
headers = {'Cookie': cookie, 'Content-Length': image_size}
|
||||
conn = self._get_http_conn('PUT', loc, headers,
|
||||
content=image_file)
|
||||
res = conn.getresponse()
|
||||
if res.status == httplib.CONFLICT:
|
||||
raise exception.Duplicate(_("Image file %(image_id)s already "
|
||||
"exists!") % {'image_id': image_id})
|
||||
|
||||
return (loc.get_uri(), image_size, checksum.hexdigest(), {})
|
||||
|
||||
def get(self, location):
|
||||
"""Takes a `glance.store.location.Location` object that indicates
|
||||
where to find the image file, and returns a tuple of generator
|
||||
(for reading the image file) and image_size
|
||||
|
||||
:param location: `glance.store.location.Location` object, supplied
|
||||
from glance.store.location.get_location_from_uri()
|
||||
"""
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
conn, resp, content_length = self._query(location,
|
||||
'GET',
|
||||
headers={'Cookie': cookie})
|
||||
iterator = http_response_iterator(conn, resp, self.CHUNKSIZE)
|
||||
|
||||
class ResponseIndexable(glance.store.Indexable):
|
||||
|
||||
def another(self):
|
||||
try:
|
||||
return self.wrapped.next()
|
||||
except StopIteration:
|
||||
return ''
|
||||
|
||||
return (ResponseIndexable(iterator, content_length), content_length)
|
||||
|
||||
def get_size(self, location):
|
||||
"""Takes a `glance.store.location.Location` object that indicates
|
||||
where to find the image file, and returns the size
|
||||
|
||||
:param location: `glance.store.location.Location` object, supplied
|
||||
from glance.store.location.get_location_from_uri()
|
||||
"""
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
|
||||
return self._query(location, 'HEAD', headers={'Cookie': cookie})[2]
|
||||
|
||||
def delete(self, location):
|
||||
"""Takes a `glance.store.location.Location` object that indicates
|
||||
where to find the image file to delete
|
||||
|
||||
:location `glance.store.location.Location` object, supplied
|
||||
from glance.store.location.get_location_from_uri()
|
||||
:raises NotFound if image does not exist
|
||||
"""
|
||||
file_path = '[%s] %s' % (
|
||||
self.datastore_name,
|
||||
location.store_location.path[len(DS_URL_PREFIX):])
|
||||
search_index_moref = self._service_content.searchIndex
|
||||
dc_moref = self._session.invoke_api(self._session.vim,
|
||||
'FindByInventoryPath',
|
||||
search_index_moref,
|
||||
inventoryPath=self.datacenter_path)
|
||||
delete_task = self._session.invoke_api(
|
||||
self._session.vim,
|
||||
'DeleteDatastoreFile_Task',
|
||||
self._service_content.fileManager,
|
||||
name=file_path,
|
||||
datacenter=dc_moref)
|
||||
self._session.wait_for_task(delete_task)
|
||||
|
||||
def _query(self, location, method, headers, depth=0):
|
||||
if depth > MAX_REDIRECTS:
|
||||
msg = (_("The HTTP URL exceeded %(max_redirects)s maximum "
|
||||
"redirects.") % {'max_redirects': MAX_REDIRECTS})
|
||||
LOG.debug(msg)
|
||||
raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
|
||||
loc = location.store_location
|
||||
conn = self._get_http_conn(method, loc, headers)
|
||||
resp = conn.getresponse()
|
||||
if resp.status >= 400:
|
||||
if resp.status == httplib.NOT_FOUND:
|
||||
msg = _('VMware datastore could not find image at URI.')
|
||||
LOG.debug(msg)
|
||||
raise exception.NotFound(msg)
|
||||
msg = (_('HTTP URL %(url)s returned a %(status)s status code.')
|
||||
% {'url': loc.get_uri(), 'status': resp.status})
|
||||
LOG.debug(msg)
|
||||
raise exception.BadStoreUri(msg)
|
||||
location_header = resp.getheader('location')
|
||||
if location_header:
|
||||
if resp.status not in (301, 302):
|
||||
msg = (_("The HTTP URL %(path)s attempted to redirect "
|
||||
"with an invalid %(status)s status code.")
|
||||
% {'path': loc.path, 'status': resp.status})
|
||||
LOG.debug(msg)
|
||||
raise exception.BadStoreUri(msg)
|
||||
location_class = glance.store.location.Location
|
||||
new_loc = location_class(location.store_name,
|
||||
location.store_location.__class__,
|
||||
uri=location_header,
|
||||
image_id=location.image_id,
|
||||
store_specs=location.store_specs)
|
||||
return self._query(new_loc, method, depth + 1)
|
||||
content_length = int(resp.getheader('content-length', 0))
|
||||
|
||||
return (conn, resp, content_length)
|
||||
|
||||
def _get_http_conn(self, method, loc, headers, content=None):
|
||||
conn_class = self._get_http_conn_class()
|
||||
conn = conn_class(loc.server_host)
|
||||
conn.request(method, '%s?%s' % (loc.path, loc.query), content, headers)
|
||||
|
||||
return conn
|
||||
|
||||
def _get_http_conn_class(self):
|
||||
if self.api_insecure:
|
||||
return httplib.HTTPConnection
|
||||
return httplib.HTTPSConnection
|
138
glance/tests/functional/store/test_vmware_datastore.py
Normal file
138
glance/tests/functional/store/test_vmware_datastore.py
Normal file
@ -0,0 +1,138 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Functional tests for the VMware Datastore store interface
|
||||
|
||||
Set the GLANCE_TEST_VMWARE_CONF environment variable to the location
|
||||
of a Glance config that defines how to connect to a functional
|
||||
VMware Datastore backend
|
||||
"""
|
||||
|
||||
import ConfigParser
|
||||
import httplib
|
||||
import os
|
||||
import urllib
|
||||
|
||||
import oslo.config.cfg
|
||||
import testtools
|
||||
|
||||
from glance.store.vmware import api
|
||||
import glance.store.vmware_datastore
|
||||
import glance.tests.functional.store as store_tests
|
||||
|
||||
|
||||
def read_config(path):
|
||||
cp = ConfigParser.RawConfigParser()
|
||||
cp.read(path)
|
||||
return cp
|
||||
|
||||
|
||||
def parse_config(config):
|
||||
out = {}
|
||||
options = [
|
||||
'vmware_server_host',
|
||||
'vmware_server_username',
|
||||
'vmware_server_password',
|
||||
'vmware_api_retry_count',
|
||||
'vmware_store_image_dir',
|
||||
'vmware_datacenter_path',
|
||||
'vmware_datastore_name',
|
||||
'vmware_api_insecure',
|
||||
]
|
||||
for option in options:
|
||||
out[option] = config.defaults()[option]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class VMwareDatastoreStoreError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def vsphere_connect(server_ip, server_username, server_password,
|
||||
api_retry_count, scheme='https',
|
||||
create_session=True, wsdl_loc=None):
|
||||
try:
|
||||
return api.VMwareAPISession(server_ip,
|
||||
server_username,
|
||||
server_password,
|
||||
api_retry_count,
|
||||
scheme=scheme,
|
||||
create_session=create_session,
|
||||
wsdl_loc=wsdl_loc)
|
||||
except AttributeError:
|
||||
raise VMwareDatastoreStoreError(
|
||||
'Could not find VMware datastore module')
|
||||
|
||||
|
||||
class TestVMwareDatastoreStore(store_tests.BaseTestCase, testtools.TestCase):
|
||||
|
||||
store_cls_path = 'glance.store.vmware_datastore.Store'
|
||||
store_cls = glance.store.vmware_datastore.Store
|
||||
store_name = 'vmware_datastore'
|
||||
|
||||
def _build_vim_cookie_header(self, vim_cookies):
|
||||
"""Build ESX host session cookie header."""
|
||||
if len(list(vim_cookies)) > 0:
|
||||
cookie = list(vim_cookies)[0]
|
||||
return cookie.name + '=' + cookie.value
|
||||
|
||||
def setUp(self):
|
||||
config_path = os.environ.get('GLANCE_TEST_VMWARE_CONF')
|
||||
if not config_path:
|
||||
msg = 'GLANCE_TEST_VMWARE_CONF environ not set.'
|
||||
self.skipTest(msg)
|
||||
|
||||
oslo.config.cfg.CONF(args=[], default_config_files=[config_path])
|
||||
|
||||
raw_config = read_config(config_path)
|
||||
config = parse_config(raw_config)
|
||||
scheme = 'http' if config['vmware_api_insecure'] == 'True' else 'https'
|
||||
self.vsphere = vsphere_connect(config['vmware_server_host'],
|
||||
config['vmware_server_username'],
|
||||
config['vmware_server_password'],
|
||||
config['vmware_api_retry_count'],
|
||||
scheme=scheme)
|
||||
|
||||
self.vmware_config = config
|
||||
super(TestVMwareDatastoreStore, self).setUp()
|
||||
|
||||
def get_store(self, **kwargs):
|
||||
store = glance.store.vmware_datastore.Store(
|
||||
context=kwargs.get('context'))
|
||||
store.configure()
|
||||
store.configure_add()
|
||||
return store
|
||||
|
||||
def stash_image(self, image_id, image_data):
|
||||
server_ip = self.vmware_config['vmware_server_host']
|
||||
path = ('/folder' + self.vmware_config['vmware_store_image_dir']
|
||||
+ '/' + image_id)
|
||||
dc_path = self.vmware_config.get('vmware_datacenter_path',
|
||||
'ha-datacenter')
|
||||
param_list = {'dcPath': dc_path,
|
||||
'dsName': self.vmware_config['vmware_datastore_name']}
|
||||
query = urllib.urlencode(param_list)
|
||||
conn = (httplib.HTTPConnection(server_ip)
|
||||
if self.vmware_config['vmware_api_insecure'] == 'True'
|
||||
else httplib.HTTPSConnection(server_ip))
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self.vsphere.vim.client.options.transport.cookiejar)
|
||||
headers = {'Cookie': cookie, 'Content-Length': len(image_data)}
|
||||
conn.request('PUT', '%s%s%s' % (path, '?', query), image_data, headers)
|
||||
conn.getresponse()
|
||||
|
||||
return 'vsphere://%s%s?%s' % (server_ip, path, query)
|
@ -23,6 +23,7 @@ from glance.openstack.common import jsonutils
|
||||
from glance import store
|
||||
from glance.store import location
|
||||
from glance.store import sheepdog
|
||||
from glance.store import vmware_datastore
|
||||
from glance.tests import stubs
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
@ -47,6 +48,8 @@ class StoreClearingUnitTest(test_utils.BaseTestCase):
|
||||
on collie.
|
||||
"""
|
||||
self.stubs.Set(sheepdog.Store, 'configure_add', lambda x: None)
|
||||
self.stubs.Set(vmware_datastore.Store, 'configure', lambda x: None)
|
||||
self.stubs.Set(vmware_datastore.Store, 'configure_add', lambda x: None)
|
||||
store.create_stores()
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@ import glance.store.http
|
||||
import glance.store.location as location
|
||||
import glance.store.s3
|
||||
import glance.store.swift
|
||||
import glance.store.vmware_datastore
|
||||
from glance.tests.unit import base
|
||||
|
||||
|
||||
@ -53,6 +54,7 @@ class TestStoreLocation(base.StoreClearingUnitTest):
|
||||
'rbd://%2F/%2F/%2F/%2F',
|
||||
'sheepdog://imagename',
|
||||
'cinder://12345678-9012-3455-6789-012345678901',
|
||||
'vsphere://ip/folder/openstack_glance/2332298?dcPath=dc&dsName=ds',
|
||||
]
|
||||
|
||||
for uri in good_store_uris:
|
||||
@ -377,6 +379,27 @@ class TestStoreLocation(base.StoreClearingUnitTest):
|
||||
bad_uri = 'http://image'
|
||||
self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
|
||||
|
||||
def test_vmware_store_location(self):
|
||||
"""
|
||||
Test the specific StoreLocation for the VMware store
|
||||
"""
|
||||
uri = ('vsphere://127.0.0.1/folder/'
|
||||
'openstack_glance/29038321?dcPath=my-dc&dsName=my-ds')
|
||||
loc = glance.store.vmware_datastore.StoreLocation({})
|
||||
loc.parse_uri(uri)
|
||||
|
||||
self.assertEqual("vsphere", loc.scheme)
|
||||
self.assertEqual("127.0.0.1", loc.server_host)
|
||||
self.assertEqual("/folder/openstack_glance/29038321", loc.path)
|
||||
self.assertEqual("dcPath=my-dc&dsName=my-ds", loc.query)
|
||||
self.assertEqual(uri, loc.get_uri())
|
||||
|
||||
bad_uri = 'vphere://'
|
||||
self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
|
||||
|
||||
bad_uri = 'http://image'
|
||||
self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
|
||||
|
||||
def test_cinder_store_good_location(self):
|
||||
"""
|
||||
Test the specific StoreLocation for the Cinder store
|
||||
@ -412,7 +435,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
|
||||
'https': glance.store.http.Store,
|
||||
'rbd': glance.store.rbd.Store,
|
||||
'sheepdog': glance.store.sheepdog.Store,
|
||||
'cinder': glance.store.cinder.Store}
|
||||
'cinder': glance.store.cinder.Store,
|
||||
'vsphere': glance.store.vmware_datastore.Store}
|
||||
|
||||
ctx = context.RequestContext()
|
||||
for scheme, store in good_results.items():
|
||||
|
212
glance/tests/unit/test_vmware_store.py
Normal file
212
glance/tests/unit/test_vmware_store.py
Normal file
@ -0,0 +1,212 @@
|
||||
# Copyright 2014 OpenStack, LLC
|
||||
# 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.
|
||||
|
||||
"""Tests the VMware Datastore backend store"""
|
||||
|
||||
import hashlib
|
||||
import StringIO
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
|
||||
from glance.common import exception
|
||||
from glance.openstack.common import units
|
||||
from glance.store.location import get_location_from_uri
|
||||
from glance.store.vmware_datastore import Store
|
||||
from glance.tests.unit import base
|
||||
from glance.tests import utils
|
||||
|
||||
|
||||
FAKE_UUID = str(uuid.uuid4())
|
||||
|
||||
FIVE_KB = 5 * units.Ki
|
||||
|
||||
VMWARE_DATASTORE_CONF = {
|
||||
'verbose': True,
|
||||
'debug': True,
|
||||
'known_stores': ['glance.store.vmware_datastore.Store'],
|
||||
'default_store': 'vsphere',
|
||||
'vmware_server_host': '127.0.0.1',
|
||||
'vmware_server_username': 'username',
|
||||
'vmware_server_password': 'password',
|
||||
'vmware_datacenter_path': 'dc1',
|
||||
'vmware_datastore_name': 'ds1',
|
||||
'vmware_store_image_dir': '/openstack_glance',
|
||||
'vmware_api_insecure': 'True'
|
||||
}
|
||||
|
||||
|
||||
def format_location(host_ip, folder_name,
|
||||
image_id, datacenter_path, datastore_name):
|
||||
"""
|
||||
Helper method that returns a VMware Datastore store URI given
|
||||
the component pieces.
|
||||
"""
|
||||
scheme = 'vsphere'
|
||||
return ("%s://%s/folder%s/%s?dsName=%s&dcPath=%s"
|
||||
% (scheme, host_ip, folder_name,
|
||||
image_id, datastore_name, datacenter_path))
|
||||
|
||||
|
||||
class FakeHTTPConnection(object):
|
||||
|
||||
def __init__(self, status=200, *args, **kwargs):
|
||||
self.status = status
|
||||
pass
|
||||
|
||||
def getresponse(self):
|
||||
return utils.FakeHTTPResponse(status=self.status)
|
||||
|
||||
def request(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class TestStore(base.StoreClearingUnitTest):
|
||||
|
||||
@mock.patch('glance.store.vmware.api.VMwareAPISession', autospec=True)
|
||||
def setUp(self, mock_session):
|
||||
"""Establish a clean test environment"""
|
||||
super(TestStore, self).setUp()
|
||||
Store.CHUNKSIZE = 2
|
||||
self.store = Store()
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
self.vim = FakeVim()
|
||||
|
||||
class FakeVim:
|
||||
def __init__(self):
|
||||
self.client = FakeClient()
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self):
|
||||
self.options = FakeOptions()
|
||||
|
||||
class FakeOptions:
|
||||
def __init__(self):
|
||||
self.transport = FakeTransport()
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self):
|
||||
self.cookiejar = FakeCookieJar()
|
||||
|
||||
class FakeCookieJar:
|
||||
pass
|
||||
|
||||
with mock.patch.object(Store, 'configure') as store:
|
||||
self.store.scheme = VMWARE_DATASTORE_CONF['default_store']
|
||||
self.store.server_host = (
|
||||
VMWARE_DATASTORE_CONF['vmware_server_host'])
|
||||
self.store.datacenter_path = (
|
||||
VMWARE_DATASTORE_CONF['vmware_datacenter_path'])
|
||||
self.store.datastore_name = (
|
||||
VMWARE_DATASTORE_CONF['vmware_datastore_name'])
|
||||
self.store.api_insecure = (
|
||||
VMWARE_DATASTORE_CONF['vmware_api_insecure'])
|
||||
self.store._session = FakeSession()
|
||||
self.store._session.invoke_api = mock.Mock()
|
||||
self.store._session.wait_for_task = mock.Mock()
|
||||
|
||||
self.store.store_image_dir = (
|
||||
VMWARE_DATASTORE_CONF['vmware_store_image_dir'])
|
||||
Store._build_vim_cookie_header = mock.Mock()
|
||||
self.addCleanup(self.stubs.UnsetAll)
|
||||
|
||||
def test_get(self):
|
||||
"""Test a "normal" retrieval of an image in chunks"""
|
||||
expected_image_size = 31
|
||||
expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
|
||||
'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
|
||||
loc = get_location_from_uri(
|
||||
"vsphere://127.0.0.1/folder/openstack_glance/%s"
|
||||
"?dsName=ds1&dcPath=dc1" % FAKE_UUID)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection()
|
||||
(image_file, image_size) = self.store.get(loc)
|
||||
self.assertEqual(image_size, expected_image_size)
|
||||
chunks = [c for c in image_file]
|
||||
self.assertEqual(chunks, expected_returns)
|
||||
|
||||
def test_get_non_existing(self):
|
||||
"""
|
||||
Test that trying to retrieve an image that doesn't exist
|
||||
raises an error
|
||||
"""
|
||||
loc = get_location_from_uri("vsphere://127.0.0.1/folder/openstack_glan"
|
||||
"ce/%s?dsName=ds1&dcPath=dc1" % FAKE_UUID)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection(status=404)
|
||||
self.assertRaises(exception.NotFound, self.store.get, loc)
|
||||
|
||||
def test_add(self):
|
||||
"""Test that we can add an image via the VMware backend"""
|
||||
expected_image_id = str(uuid.uuid4())
|
||||
expected_size = FIVE_KB
|
||||
expected_contents = "*" * expected_size
|
||||
hash_code = hashlib.md5(expected_contents)
|
||||
expected_checksum = hash_code.hexdigest()
|
||||
hashlib.md5 = mock.Mock(return_value=hash_code)
|
||||
expected_location = format_location(
|
||||
VMWARE_DATASTORE_CONF['vmware_server_host'],
|
||||
VMWARE_DATASTORE_CONF['vmware_store_image_dir'],
|
||||
expected_image_id,
|
||||
VMWARE_DATASTORE_CONF['vmware_datacenter_path'],
|
||||
VMWARE_DATASTORE_CONF['vmware_datastore_name'])
|
||||
image = StringIO.StringIO(expected_contents)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection()
|
||||
location, size, checksum, _ = self.store.add(expected_image_id,
|
||||
image,
|
||||
expected_size)
|
||||
self.assertEqual(expected_location, location)
|
||||
self.assertEqual(expected_size, size)
|
||||
self.assertEqual(expected_checksum, checksum)
|
||||
|
||||
def test_delete(self):
|
||||
"""Test we can delete an existing image in the VMware store"""
|
||||
loc = get_location_from_uri(
|
||||
"vsphere://127.0.0.1/folder/openstack_glance/%s?"
|
||||
"dsName=ds1&dcPath=dc1" % FAKE_UUID)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection()
|
||||
Store._service_content = mock.Mock()
|
||||
self.store.delete(loc)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection(status=404)
|
||||
self.assertRaises(exception.NotFound, self.store.get, loc)
|
||||
|
||||
def test_get_size(self):
|
||||
"""Test we can get the size of an existing image in the VMware store"""
|
||||
loc = get_location_from_uri(
|
||||
"vsphere://127.0.0.1/folder/openstack_glance/%s"
|
||||
"?dsName=ds1&dcPath=dc1" % FAKE_UUID)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection()
|
||||
image_size = self.store.get_size(loc)
|
||||
self.assertEqual(image_size, 31)
|
||||
|
||||
def test_get_size_non_existing(self):
|
||||
"""
|
||||
Test that trying to retrieve an image size that doesn't exist
|
||||
raises an error
|
||||
"""
|
||||
loc = get_location_from_uri("vsphere://127.0.0.1/folder/openstack_glan"
|
||||
"ce/%s?dsName=ds1&dcPath=dc1" % FAKE_UUID)
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection(status=404)
|
||||
self.assertRaises(exception.NotFound, self.store.get_size, loc)
|
@ -22,6 +22,7 @@ iso8601>=0.1.8
|
||||
ordereddict
|
||||
oslo.config>=1.2.0
|
||||
stevedore>=0.12
|
||||
suds>=0.4
|
||||
|
||||
# For Swift storage backend.
|
||||
python-swiftclient>=1.5
|
||||
|
Loading…
Reference in New Issue
Block a user