Implements APIs for VMDK driver

- Implemented APIs: create_volume, delete_volume, initialize_connection,
  create_snapthot, delete_snapshot, create_volume_from_snapshot,
  create_cloned_volume
- Modified etc/cinder/cinder.conf.sample adding details for the driver
- Added suds dependency in requirements.txt
- create_volume: does no work. Volume's backing is created lazily
- delete_volume: delete backing if present
- initialize_connection: if backing, not present, create backing, else
  provide backing details to nova to perform attach
  Nova BP/vmware-nova-cinder-support
- create_snapshot: Creates the snapshot of the backing
- delete_snapshot: Deletes the snapshot of the backing
- create_volume_from_snapshot: Creates a full/linked clone from the snapshot
  point in VC. In ESX, copies the VM backing files, registers and reverts to
  the appropriate snapshot point.
- create_cloned_volume: Creates a full/linked clone in VC. In ESX, copies the
  VM backing files and registers as a new backing.
- Written appropriate unit tests
- Work item in BP/vmware-vmdk-cinder-driver

Implements: blueprint vmware-vmdk-cinder-driver
Change-Id: Ib11f2878f8f656209d1ba5e2cbfadae1ac1999b4
This commit is contained in:
Kartik Bommepally 2013-08-12 21:42:59 -07:00
parent 9afb7718c3
commit 09bc926460
10 changed files with 3595 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
# Copyright (c) 2013 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.
"""
:mod:`vmware` -- Volume support for VMware compatible datastores.
"""

View File

@ -0,0 +1,273 @@
# vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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 cinder.volume.drivers.vmware.vim.Vim SOAP calls.
"""
from eventlet import event
from cinder.openstack.common import log as logging
from cinder.openstack.common import loopingcall
from cinder.volume.drivers.vmware import error_util
from cinder.volume.drivers.vmware import vim
from cinder.volume.drivers.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."""
@Retry(exceptions=(Exception))
def __init__(self, server_ip, server_username, server_password,
api_retry_count, task_poll_interval, 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 task_poll_interval: Sleep time in seconds for polling an
on-going async task as part of the API call
: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
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.exception(_("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 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 event: 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)

View File

@ -0,0 +1,49 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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 cinder import exception
NOT_AUTHENTICATED = 'NotAuthenticated'
class VimException(exception.CinderException):
"""The VIM Exception class."""
def __init__(self, msg):
exception.CinderException.__init__(self, msg)
class SessionOverLoadException(VimException):
"""Session Overload Exception."""
pass
class VimAttributeException(VimException):
"""VI Attribute Error."""
pass
class VimFaultException(exception.VolumeBackendAPIException):
"""The VIM Fault exception class."""
def __init__(self, fault_list, msg):
exception.VolumeBackendAPIException.__init__(self, msg)
self.fault_list = fault_list

View File

@ -0,0 +1,236 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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 suds
from cinder.volume.drivers.vmware import error_util
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]
"""
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_fault_checker(response):
"""Checks the RetrieveProperties 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 RetrieveProperties API call
"""
fault_list = []
if not response:
# This is the case when the session has timed out. ESX SOAP
# server sends an empty RetrievePropertiesResponse. 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 "
"RetrieveProperties.") %
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() == 'retrieveproperties'):
retrieve_properties_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."

View File

@ -0,0 +1,252 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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, props_to_collect=None, all_properties=False):
"""Gets all managed object references of a specified type.
:param vim: Vim object
:param type: Type of the managed object reference
: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])
return vim.RetrieveProperties(vim.service_content.propertyCollector,
specSet=[property_filter_spec])
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]
return vim.RetrieveProperties(collector, specSet=[property_filter_spec])
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

View File

@ -0,0 +1,727 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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.
"""
Driver for virtual machines running on VMware supported datastores.
"""
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import units
from cinder.volume import driver
from cinder.volume.drivers.vmware import api
from cinder.volume.drivers.vmware import error_util
from cinder.volume.drivers.vmware import vim
from cinder.volume.drivers.vmware import volumeops
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
THIN_VMDK_TYPE = 'thin'
THICK_VMDK_TYPE = 'thick'
EAGER_ZEROED_THICK_VMDK_TYPE = 'eagerZeroedThick'
vmdk_opts = [
cfg.StrOpt('vmware_host_ip',
default=None,
help='IP address for connecting to VMware ESX/VC server.'),
cfg.StrOpt('vmware_host_username',
default=None,
help='Username for authenticating with VMware ESX/VC server.'),
cfg.StrOpt('vmware_host_password',
default=None,
help='Password for authenticating with VMware ESX/VC server.',
secret=True),
cfg.StrOpt('vmware_wsdl_location',
default=None,
help='Optional VIM service WSDL Location '
'e.g http://<server>/vimService.wsdl. Optional over-ride '
'to default location for bug work-arounds.'),
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_volume_folder',
default='cinder-volumes',
help='Name for the folder in the VC datacenter that will '
'contain cinder volumes.')
]
def _get_volume_type_extra_spec(type_id, spec_key, possible_values,
default_value):
"""Get extra spec value.
If the spec value is not present in the input possible_values, then
default_value will be returned.
If the type_id is None, then default_value is returned.
The caller must not consider scope and the implementation adds/removes
scope. The scope used here is 'vmware' e.g. key 'vmware:vmdk_type' and
so the caller must pass vmdk_type as an input ignoring the scope.
:param type_id: Volume type ID
:param spec_key: Extra spec key
:param possible_values: Permitted values for the extra spec
:param default_value: Default value for the extra spec incase of an
invalid value or if the entry does not exist
:return: extra spec value
"""
if type_id:
spec_key = ('vmware:%s') % spec_key
spec_value = volume_types.get_volume_type_extra_specs(type_id,
spec_key)
if spec_value in possible_values:
LOG.debug(_("Returning spec value %s") % spec_value)
return spec_value
LOG.debug(_("Invalid spec value: %s specified.") % spec_value)
# Default we return thin disk type
LOG.debug(_("Returning default spec value: %s.") % default_value)
return default_value
class VMwareEsxVmdkDriver(driver.VolumeDriver):
"""Manage volumes on VMware ESX server."""
VERSION = '1.0'
def __init__(self, *args, **kwargs):
super(VMwareEsxVmdkDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(vmdk_opts)
self._session = None
self._stats = None
self._volumeops = None
@property
def session(self):
if not self._session:
ip = self.configuration.vmware_host_ip
username = self.configuration.vmware_host_username
password = self.configuration.vmware_host_password
api_retry_count = self.configuration.vmware_api_retry_count
task_poll_interval = self.configuration.vmware_task_poll_interval
wsdl_loc = self.configuration.safe_get('vmware_wsdl_location')
self._session = api.VMwareAPISession(ip, username,
password, api_retry_count,
task_poll_interval,
wsdl_loc=wsdl_loc)
return self._session
@property
def volumeops(self):
if not self._volumeops:
self._volumeops = volumeops.VMwareVolumeOps(self.session)
return self._volumeops
def do_setup(self, context):
"""Perform validations and establish connection to server.
:param context: Context information
"""
# Throw error if required parameters are not set.
required_params = ['vmware_host_ip',
'vmware_host_username',
'vmware_host_password']
for param in required_params:
if not getattr(self.configuration, param, None):
raise exception.InvalidInput(_("%s not set.") % param)
# Create the session object for the first time
self._volumeops = volumeops.VMwareVolumeOps(self.session)
LOG.info(_("Successfully setup driver: %(driver)s for "
"server: %(ip)s.") %
{'driver': self.__class__.__name__,
'ip': self.configuration.vmware_host_ip})
def check_for_setup_error(self):
pass
def get_volume_stats(self, refresh=False):
"""Obtain status of the volume service.
:param refresh: Whether to get refreshed information
"""
if not self._stats:
backend_name = self.configuration.safe_get('volume_backend_name')
if not backend_name:
backend_name = self.__class__.__name__
data = {'volume_backend_name': backend_name,
'vendor_name': 'VMware',
'driver_version': self.VERSION,
'storage_protocol': 'LSI Logic SCSI',
'reserved_percentage': 0,
'total_capacity_gb': 'unknown',
'free_capacity_gb': 'unknown'}
self._stats = data
return self._stats
def create_volume(self, volume):
"""Creates a volume.
We do not create any backing. We do it only for the first time
it is being attached to a virtual machine.
:param volume: Volume object
"""
pass
def _delete_volume(self, volume):
"""Delete the volume backing if it is present.
:param volume: Volume object
"""
backing = self.volumeops.get_backing(volume['name'])
if not backing:
LOG.info(_("Backing not available, no operation to be performed."))
return
self.volumeops.delete_backing(backing)
def delete_volume(self, volume):
"""Deletes volume backing.
:param volume: Volume object
"""
self._delete_volume(volume)
def _get_volume_group_folder(self, datacenter):
"""Return vmFolder of datacenter as we cannot create folder in ESX.
:param datacenter: Reference to the datacenter
:return: vmFolder reference of the datacenter
"""
return self.volumeops.get_vmfolder(datacenter)
def _select_datastore_summary(self, size_bytes, datastores):
"""Get best summary from datastore list that can accomodate volume.
The implementation selects datastore based on maximum relative
free space, which is (free_space/total_space) and has free space to
store the volume backing.
:param size_bytes: Size in bytes of the volume
:param datastores: Datastores from which a choice is to be made
for the volume
:return: Best datastore summary to be picked for the volume
"""
best_summary = None
best_ratio = 0
for datastore in datastores:
summary = self.volumeops.get_summary(datastore)
if summary.freeSpace > size_bytes:
ratio = float(summary.freeSpace) / summary.capacity
if ratio > best_ratio:
best_ratio = ratio
best_summary = summary
if not best_summary:
msg = _("Unable to pick datastore to accomodate %(size)s bytes "
"from the datastores: %(dss)s.")
LOG.error(msg % {'size': size_bytes, 'dss': datastores})
raise error_util.VimException(msg %
{'size': size_bytes,
'dss': datastores})
LOG.debug(_("Selected datastore: %s for the volume.") % best_summary)
return best_summary
def _get_folder_ds_summary(self, size_gb, resource_pool, datastores):
"""Get folder and best datastore summary where volume can be placed.
:param size_gb: Size of the volume in GB
:param resource_pool: Resource pool reference
:param datastores: Datastores from which a choice is to be made
for the volume
:return: Folder and best datastore summary where volume can be
placed on
"""
datacenter = self.volumeops.get_dc(resource_pool)
folder = self._get_volume_group_folder(datacenter)
size_bytes = size_gb * units.GiB
datastore_summary = self._select_datastore_summary(size_bytes,
datastores)
return (folder, datastore_summary)
@staticmethod
def _get_disk_type(volume):
"""Get disk type from volume type.
:param volume: Volume object
:return: Disk type
"""
return _get_volume_type_extra_spec(volume['volume_type_id'],
'vmdk_type',
(THIN_VMDK_TYPE, THICK_VMDK_TYPE,
EAGER_ZEROED_THICK_VMDK_TYPE),
THIN_VMDK_TYPE)
def _create_backing(self, volume, host):
"""Create volume backing under the given host.
:param volume: Volume object
:param host: Reference of the host
:return: Reference to the created backing
"""
# Get datastores and resource pool of the host
(datastores, resource_pool) = self.volumeops.get_dss_rp(host)
# Pick a folder and datastore to create the volume backing on
(folder, summary) = self._get_folder_ds_summary(volume['size'],
resource_pool,
datastores)
disk_type = VMwareEsxVmdkDriver._get_disk_type(volume)
size_kb = volume['size'] * units.MiB
return self.volumeops.create_backing(volume['name'],
size_kb,
disk_type, folder,
resource_pool,
host,
summary.name)
def _relocate_backing(self, size_gb, backing, host):
pass
def _create_backing_in_inventory(self, volume):
"""Creates backing under any suitable host.
The method tries to pick datastore that can fit the volume under
any host in the inventory.
:param volume: Volume object
:return: Reference to the created backing
"""
# Get all hosts
hosts = self.volumeops.get_hosts()
if not hosts:
msg = _("There are no hosts in the inventory.")
LOG.error(msg)
raise error_util.VimException(msg)
backing = None
for host in hosts:
try:
host = hosts[0].obj
backing = self._create_backing(volume, host)
break
except error_util.VimException as excep:
LOG.warn(_("Unable to find suitable datastore for "
"volume: %(vol)s under host: %(host)s. "
"More details: %(excep)s") %
{'vol': volume['name'], 'host': host, 'excep': excep})
if backing:
return backing
msg = _("Unable to create volume: %(vol)s on the hosts: %(hosts)s.")
LOG.error(msg % {'vol': volume['name'], 'hosts': hosts})
raise error_util.VimException(msg %
{'vol': volume['name'], 'hosts': hosts})
def _initialize_connection(self, volume, connector):
"""Get information of volume's backing.
If the volume does not have a backing yet. It will be created.
:param volume: Volume object
:param connector: Connector information
:return: Return connection information
"""
connection_info = {'driver_volume_type': 'vmdk'}
backing = self.volumeops.get_backing(volume['name'])
if 'instance' in connector:
# The instance exists
instance = vim.get_moref(connector['instance'], 'VirtualMachine')
LOG.debug(_("The instance: %s for which initialize connection "
"is called, exists.") % instance)
# Get host managing the instance
host = self.volumeops.get_host(instance)
if not backing:
# Create a backing in case it does not exist under the
# host managing the instance.
LOG.info(_("There is no backing for the volume: %s. "
"Need to create one.") % volume['name'])
backing = self._create_backing(volume, host)
else:
# Relocate volume is necessary
self._relocate_backing(volume['size'], backing, host)
else:
# The instance does not exist
LOG.debug(_("The instance for which initialize connection "
"is called, does not exist."))
if not backing:
# Create a backing in case it does not exist. It is a bad use
# case to boot from an empty volume.
LOG.warn(_("Trying to boot from an empty volume: %s.") %
volume['name'])
# Create backing
backing = self._create_backing_in_inventory(volume)
# Set volume's moref value and name
connection_info['data'] = {'volume': backing.value,
'volume_id': volume['id']}
LOG.info(_("Returning connection_info: %(info)s for volume: "
"%(volume)s with connector: %(connector)s.") %
{'info': connection_info,
'volume': volume['name'],
'connector': connector})
return connection_info
def initialize_connection(self, volume, connector):
"""Allow connection to connector and return connection info.
The implementation returns the following information:
{'driver_volume_type': 'vmdk'
'data': {'volume': $VOLUME_MOREF_VALUE
'volume_id': $VOLUME_ID
}
}
:param volume: Volume object
:param connector: Connector information
:return: Return connection information
"""
return self._initialize_connection(volume, connector)
def terminate_connection(self, volume, connector, force=False, **kwargs):
pass
def create_export(self, context, volume):
pass
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass
def _create_snapshot(self, snapshot):
"""Creates a snapshot.
If the volume does not have a backing then simply pass, else create
a snapshot.
:param snapshot: Snapshot object
"""
backing = self.volumeops.get_backing(snapshot['volume_name'])
if not backing:
LOG.info(_("There is no backing, so will not create "
"snapshot: %s.") % snapshot['name'])
return
self.volumeops.create_snapshot(backing, snapshot['name'],
snapshot['display_description'])
LOG.info(_("Successfully created snapshot: %s.") % snapshot['name'])
def create_snapshot(self, snapshot):
"""Creates a snapshot.
:param snapshot: Snapshot object
"""
self._create_snapshot(snapshot)
def _delete_snapshot(self, snapshot):
"""Delete snapshot.
If the volume does not have a backing or the snapshot does not exist
then simply pass, else delete the snapshot.
:param snapshot: Snapshot object
"""
backing = self.volumeops.get_backing(snapshot['volume_name'])
if not backing:
LOG.info(_("There is no backing, and so there is no "
"snapshot: %s.") % snapshot['name'])
else:
self.volumeops.delete_snapshot(backing, snapshot['name'])
LOG.info(_("Successfully deleted snapshot: %s.") %
snapshot['name'])
def delete_snapshot(self, snapshot):
"""Delete snapshot.
:param snapshot: Snapshot object
"""
self._delete_snapshot(snapshot)
def _clone_backing_by_copying(self, volume, backing):
"""Creates volume clone.
Here we copy the backing on a datastore under the host and then
register the copied backing to the inventory.
It is assumed here that all the source backing files are in the
same folder on the datastore.
:param volume: New Volume object
:param backing: Reference to backing entity that must be cloned
:return: Reference to the cloned backing
"""
src_path_name = self.volumeops.get_path_name(backing)
# If we have path like /vmfs/volumes/datastore/vm/vm.vmx
# we need to use /vmfs/volumes/datastore/vm/ are src_path
splits = src_path_name.split('/')
last_split = splits[len(splits) - 1]
src_path = src_path_name[:-len(last_split)]
# Pick a datastore where to create the full clone under same host
host = self.volumeops.get_host(backing)
(datastores, resource_pool) = self.volumeops.get_dss_rp(host)
(folder, summary) = self._get_folder_ds_summary(volume['size'],
resource_pool,
datastores)
dest_path = '[%s] %s' % (summary.name, volume['name'])
# Copy source backing files to a destination location
self.volumeops.copy_backing(src_path, dest_path)
# Register the backing to the inventory
dest_path_name = '%s/%s' % (dest_path, last_split)
clone = self.volumeops.register_backing(dest_path_name,
volume['name'], folder,
resource_pool)
LOG.info(_("Successfully cloned new backing: %s.") % clone)
return clone
def _create_cloned_volume(self, volume, src_vref):
"""Creates volume clone.
If source volume's backing does not exist, then pass.
Here we copy the backing on a datastore under the host and then
register the copied backing to the inventory.
It is assumed here that all the src_vref backing files are in the
same folder on the datastore.
:param volume: New Volume object
:param src_vref: Volume object that must be cloned
"""
backing = self.volumeops.get_backing(src_vref['name'])
if not backing:
LOG.info(_("There is no backing for the source volume: "
"%(svol)s. Not creating any backing for the "
"volume: %(vol)s.") %
{'svol': src_vref['name'],
'vol': volume['name']})
return
self._clone_backing_by_copying(volume, backing)
def create_cloned_volume(self, volume, src_vref):
"""Creates volume clone.
:param volume: New Volume object
:param src_vref: Volume object that must be cloned
"""
self._create_cloned_volume(volume, src_vref)
def _create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.
If the snapshot does not exist or source volume's backing does not
exist, then pass.
Else we perform _create_cloned_volume and then revert the backing to
the appropriate snapshot point.
:param volume: Volume object
:param snapshot: Snapshot object
"""
backing = self.volumeops.get_backing(snapshot['volume_name'])
if not backing:
LOG.info(_("There is no backing for the source snapshot: "
"%(snap)s. Not creating any backing for the "
"volume: %(vol)s.") %
{'snap': snapshot['name'],
'vol': volume['name']})
return
snapshot_moref = self.volumeops.get_snapshot(backing,
snapshot['name'])
if not snapshot_moref:
LOG.info(_("There is no snapshot point for the snapshoted volume: "
"%(snap)s. Not creating any backing for the "
"volume: %(vol)s.") %
{'snap': snapshot['name'], 'vol': volume['name']})
return
clone = self._clone_backing_by_copying(volume, backing)
# Reverting the clone to the snapshot point.
snapshot_moref = self.volumeops.get_snapshot(clone, snapshot['name'])
self.volumeops.revert_to_snapshot(snapshot_moref)
LOG.info(_("Successfully reverted clone: %(clone)s to snapshot: "
"%(snapshot)s.") %
{'clone': clone, 'snapshot': snapshot_moref})
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.
:param volume: Volume object
:param snapshot: Snapshot object
"""
self._create_volume_from_snapshot(volume, snapshot)
class VMwareVcVmdkDriver(VMwareEsxVmdkDriver):
"""Manage volumes on VMware VC server."""
def _get_volume_group_folder(self, datacenter):
"""Get volume group folder.
Creates a folder under the vmFolder of the input datacenter with the
volume group name if it does not exists.
:param datacenter: Reference to the datacenter
:return: Reference to the volume folder
"""
vm_folder = super(VMwareVcVmdkDriver,
self)._get_volume_group_folder(datacenter)
volume_folder = self.configuration.vmware_volume_folder
return self.volumeops.create_folder(vm_folder, volume_folder)
def _relocate_backing(self, size_gb, backing, host):
"""Relocate volume backing under host and move to volume_group folder.
If the volume backing is on a datastore that is visible to the host,
then need not do any operation.
:param size_gb: Size of the volume in GB
:param backing: Reference to the backing
:param host: Reference to the host
"""
# Check if volume's datastore is visible to host managing
# the instance
(datastores, resource_pool) = self.volumeops.get_dss_rp(host)
datastore = self.volumeops.get_datastore(backing)
visible_to_host = False
for _datastore in datastores:
if _datastore.value == datastore.value:
visible_to_host = True
break
if visible_to_host:
return
# The volume's backing is on a datastore that is not visible to the
# host managing the instance. We relocate the volume's backing.
# Pick a folder and datastore to relocate volume backing to
(folder, summary) = self._get_folder_ds_summary(size_gb, resource_pool,
datastores)
LOG.info(_("Relocating volume: %(backing)s to %(ds)s and %(rp)s.") %
{'backing': backing, 'ds': summary, 'rp': resource_pool})
# Relocate the backing to the datastore and folder
self.volumeops.relocate_backing(backing, summary.datastore,
resource_pool, host)
self.volumeops.move_backing_to_folder(backing, folder)
@staticmethod
def _get_clone_type(volume):
"""Get clone type from volume type.
:param volume: Volume object
:return: Clone type from the extra spec if present, else return
default 'full' clone type
"""
return _get_volume_type_extra_spec(volume['volume_type_id'],
'clone_type',
(volumeops.FULL_CLONE_TYPE,
volumeops.LINKED_CLONE_TYPE),
volumeops.FULL_CLONE_TYPE)
def _clone_backing(self, volume, backing, snapshot, clone_type):
"""Clone the backing.
:param volume: New Volume object
:param backing: Reference to the backing entity
:param snapshot: Reference to snapshot entity
:param clone_type: type of the clone
"""
datastore = None
if not clone_type == volumeops.LINKED_CLONE_TYPE:
# Pick a datastore where to create the full clone under same host
host = self.volumeops.get_host(backing)
(datastores, resource_pool) = self.volumeops.get_dss_rp(host)
size_bytes = volume['size'] * units.GiB
datastore = self._select_datastore_summary(size_bytes,
datastores).datastore
clone = self.volumeops.clone_backing(volume['name'], backing,
snapshot, clone_type, datastore)
LOG.info(_("Successfully created clone: %s.") % clone)
def _create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.
If the snapshot does not exist or source volume's backing does not
exist, then pass.
:param volume: New Volume object
:param snapshot: Reference to snapshot entity
"""
backing = self.volumeops.get_backing(snapshot['volume_name'])
if not backing:
LOG.info(_("There is no backing for the snapshoted volume: "
"%(snap)s. Not creating any backing for the "
"volume: %(vol)s.") %
{'snap': snapshot['name'], 'vol': volume['name']})
return
snapshot_moref = self.volumeops.get_snapshot(backing,
snapshot['name'])
if not snapshot_moref:
LOG.info(_("There is no snapshot point for the snapshoted volume: "
"%(snap)s. Not creating any backing for the "
"volume: %(vol)s.") %
{'snap': snapshot['name'], 'vol': volume['name']})
return
clone_type = VMwareVcVmdkDriver._get_clone_type(volume)
self._clone_backing(volume, backing, snapshot_moref, clone_type)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.
:param volume: New Volume object
:param snapshot: Reference to snapshot entity
"""
self._create_volume_from_snapshot(volume, snapshot)
def _create_cloned_volume(self, volume, src_vref):
"""Creates volume clone.
If source volume's backing does not exist, then pass.
:param volume: New Volume object
:param src_vref: Source Volume object
"""
backing = self.volumeops.get_backing(src_vref['name'])
if not backing:
LOG.info(_("There is no backing for the source volume: %(src)s. "
"Not creating any backing for volume: %(vol)s.") %
{'src': src_vref['name'], 'vol': volume['name']})
return
clone_type = VMwareVcVmdkDriver._get_clone_type(volume)
snapshot = None
if clone_type == volumeops.LINKED_CLONE_TYPE:
# For performing a linked clone, we snapshot the volume and
# then create the linked clone out of this snapshot point.
name = 'snapshot-%s' % volume['id']
snapshot = self.volumeops.create_snapshot(backing, name, None)
self._clone_backing(volume, backing, snapshot, clone_type)
def create_cloned_volume(self, volume, src_vref):
"""Creates volume clone.
:param volume: New Volume object
:param src_vref: Source Volume object
"""
self._create_cloned_volume(volume, src_vref)

View File

@ -0,0 +1,606 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 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.
"""
Implements operations on volumes residing on VMware datastores.
"""
from cinder.openstack.common import log as logging
from cinder.volume.drivers.vmware import error_util
from cinder.volume.drivers.vmware import vim_util
LOG = logging.getLogger(__name__)
LINKED_CLONE_TYPE = 'linked'
FULL_CLONE_TYPE = 'full'
ALREADY_EXISTS = 'AlreadyExists'
FILE_ALREADY_EXISTS = 'FileAlreadyExists'
class VMwareVolumeOps(object):
"""Manages volume operations."""
def __init__(self, session):
self._session = session
def get_backing(self, name):
"""Get the backing based on name.
:param name: Name of the backing
:return: Managed object reference to the backing
"""
vms = self._session.invoke_api(vim_util, 'get_objects',
self._session.vim, 'VirtualMachine')
for vm in vms:
if vm.propSet[0].val == name:
return vm.obj
LOG.debug(_("Did not find any backing with name: %s") % name)
def delete_backing(self, backing):
"""Delete the backing.
:param backing: Managed object reference to the backing
"""
LOG.debug(_("Deleting the VM backing: %s.") % backing)
task = self._session.invoke_api(self._session.vim, 'Destroy_Task',
backing)
LOG.debug(_("Initiated deletion of VM backing: %s.") % backing)
self._session.wait_for_task(task)
LOG.info(_("Deleted the VM backing: %s.") % backing)
# TODO(kartikaditya) Keep the methods not specific to volume in
# a different file
def get_host(self, instance):
"""Get host under which instance is present.
:param instance: Managed object reference of the instance VM
:return: Host managing the instance VM
"""
return self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, instance,
'runtime.host')
def get_hosts(self):
"""Get all host from the inventory.
:return: All the hosts from the inventory
"""
return self._session.invoke_api(vim_util, 'get_objects',
self._session.vim, 'HostSystem')
def get_dss_rp(self, host):
"""Get datastores and resource pool of the host.
:param host: Managed object reference of the host
:return: Datastores mounted to the host and resource pool to which
the host belongs to
"""
props = self._session.invoke_api(vim_util, 'get_object_properties',
self._session.vim, host,
['datastore', 'parent'])
# Get datastores and compute resource or cluster compute resource
datastores = None
compute_resource = None
for elem in props:
for prop in elem.propSet:
if prop.name == 'datastore':
datastores = prop.val.ManagedObjectReference
elif prop.name == 'parent':
compute_resource = prop.val
# Get resource pool from compute resource or cluster compute resource
resource_pool = self._session.invoke_api(vim_util,
'get_object_property',
self._session.vim,
compute_resource,
'resourcePool')
if not datastores:
msg = _("There are no datastores present under %s.")
LOG.error(msg % host)
raise error_util.VimException(msg % host)
return (datastores, resource_pool)
def _get_parent(self, child, parent_type):
"""Get immediate parent of given type via 'parent' property.
:param child: Child entity reference
:param parent_type: Entity type of the parent
:return: Immediate parent of specific type up the hierarchy via
'parent' property
"""
if not child:
return None
if child._type == parent_type:
return child
parent = self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, child, 'parent')
return self._get_parent(parent, parent_type)
def get_dc(self, child):
"""Get parent datacenter up the hierarchy via 'parent' property.
:param child: Reference of the child entity
:return: Parent Datacenter of the param child entity
"""
return self._get_parent(child, 'Datacenter')
def get_vmfolder(self, datacenter):
"""Get the vmFolder.
:param datacenter: Reference to the datacenter entity
:return: vmFolder property of the datacenter
"""
return self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, datacenter,
'vmFolder')
def create_folder(self, parent_folder, child_folder_name):
"""Creates child folder with given name under the given parent folder.
The method first checks if a child folder already exists, if it does,
then it returns a moref for the folder, else it creates one and then
return the moref.
:param parent_folder: Reference to the folder entity
:param child_folder_name: Name of the child folder
:return: Reference to the child folder with input name if it already
exists, else create one and return the reference
"""
LOG.debug(_("Creating folder: %(child_folder_name)s under parent "
"folder: %(parent_folder)s.") %
{'child_folder_name': child_folder_name,
'parent_folder': parent_folder})
# Get list of child entites for the parent folder
prop_val = self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, parent_folder,
'childEntity')
child_entities = prop_val.ManagedObjectReference
# Return if the child folder with input name is already present
for child_entity in child_entities:
if child_entity._type != 'Folder':
continue
child_entity_name = self._session.invoke_api(vim_util,
'get_object_property',
self._session.vim,
child_entity,
'name')
if child_entity_name == child_folder_name:
LOG.debug(_("Child folder already present: %s.") %
child_entity)
return child_entity
# Need to create the child folder
child_folder = self._session.invoke_api(self._session.vim,
'CreateFolder', parent_folder,
name=child_folder_name)
LOG.debug(_("Created child folder: %s.") % child_folder)
return child_folder
def _get_create_spec(self, name, size_kb, disk_type, ds_name):
"""Return spec for creating volume backing.
:param name: Name of the backing
:param size_kb: Size in KB of the backing
:param disk_type: VMDK type for the disk
:param ds_name: Datastore name where the disk is to be provisioned
:return: Spec for creation
"""
cf = self._session.vim.client.factory
controller_device = cf.create('ns0:VirtualLsiLogicController')
controller_device.key = -100
controller_device.busNumber = 0
controller_device.sharedBus = 'noSharing'
controller_spec = cf.create('ns0:VirtualDeviceConfigSpec')
controller_spec.operation = 'add'
controller_spec.device = controller_device
disk_device = cf.create('ns0:VirtualDisk')
disk_device.capacityInKB = size_kb
disk_device.key = -101
disk_device.unitNumber = 0
disk_device.controllerKey = -100
disk_device_bkng = cf.create('ns0:VirtualDiskFlatVer2BackingInfo')
if disk_type == 'eagerZeroedThick':
disk_device_bkng.eagerlyScrub = True
elif disk_type == 'thin':
disk_device_bkng.thinProvisioned = True
disk_device_bkng.fileName = '[%s]' % ds_name
disk_device_bkng.diskMode = 'persistent'
disk_device.backing = disk_device_bkng
disk_spec = cf.create('ns0:VirtualDeviceConfigSpec')
disk_spec.operation = 'add'
disk_spec.fileOperation = 'create'
disk_spec.device = disk_device
vm_file_info = cf.create('ns0:VirtualMachineFileInfo')
vm_file_info.vmPathName = '[%s]' % ds_name
create_spec = cf.create('ns0:VirtualMachineConfigSpec')
create_spec.name = name
create_spec.guestId = 'otherGuest'
create_spec.numCPUs = 1
create_spec.memoryMB = 128
create_spec.deviceChange = [controller_spec, disk_spec]
create_spec.files = vm_file_info
LOG.debug(_("Spec for creating the backing: %s.") % create_spec)
return create_spec
def create_backing(self, name, size_kb, disk_type,
folder, resource_pool, host, ds_name):
"""Create backing for the volume.
Creates a VM with one VMDK based on the given inputs.
:param name: Name of the backing
:param size_kb: Size in KB of the backing
:param disk_type: VMDK type for the disk
:param folder: Folder, where to create the backing under
:param resource_pool: Resource pool reference
:param host: Host reference
:param ds_name: Datastore name where the disk is to be provisioned
:return: Reference to the created backing entity
"""
LOG.debug(_("Creating volume backing name: %(name)s "
"disk_type: %(disk_type)s size_kb: %(size_kb)s at "
"folder: %(folder)s resourse pool: %(resource_pool)s "
"datastore name: %(ds_name)s.") %
{'name': name, 'disk_type': disk_type, 'size_kb': size_kb,
'folder': folder, 'resource_pool': resource_pool,
'ds_name': ds_name})
create_spec = self._get_create_spec(name, size_kb, disk_type, ds_name)
task = self._session.invoke_api(self._session.vim, 'CreateVM_Task',
folder, config=create_spec,
pool=resource_pool, host=host)
LOG.debug(_("Initiated creation of volume backing: %s.") % name)
task_info = self._session.wait_for_task(task)
backing = task_info.result
LOG.info(_("Successfully created volume backing: %s.") % backing)
return backing
def get_datastore(self, backing):
"""Get datastore where the backing resides.
:param backing: Reference to the backing
:return: Datastore reference to which the backing belongs
"""
return self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, backing,
'datastore').ManagedObjectReference[0]
def get_summary(self, datastore):
"""Get datastore summary.
:param datastore: Reference to the datastore
:return: 'summary' property of the datastore
"""
return self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, datastore,
'summary')
def _get_relocate_spec(self, datastore, resource_pool, host,
disk_move_type):
"""Return spec for relocating volume backing.
:param datastore: Reference to the datastore
:param resource_pool: Reference to the resource pool
:param host: Reference to the host
:param disk_move_type: Disk move type option
:return: Spec for relocation
"""
cf = self._session.vim.client.factory
relocate_spec = cf.create('ns0:VirtualMachineRelocateSpec')
relocate_spec.datastore = datastore
relocate_spec.pool = resource_pool
relocate_spec.host = host
relocate_spec.diskMoveType = disk_move_type
LOG.debug(_("Spec for relocating the backing: %s.") % relocate_spec)
return relocate_spec
def relocate_backing(self, backing, datastore, resource_pool, host):
"""Relocates backing to the input datastore and resource pool.
The implementation uses moveAllDiskBackingsAndAllowSharing disk move
type.
:param backing: Reference to the backing
:param datastore: Reference to the datastore
:param resource_pool: Reference to the resource pool
:param host: Reference to the host
"""
LOG.debug(_("Relocating backing: %(backing)s to datastore: %(ds)s "
"and resource pool: %(rp)s.") %
{'backing': backing, 'ds': datastore, 'rp': resource_pool})
# Relocate the volume backing
disk_move_type = 'moveAllDiskBackingsAndAllowSharing'
relocate_spec = self._get_relocate_spec(datastore, resource_pool, host,
disk_move_type)
task = self._session.invoke_api(self._session.vim, 'RelocateVM_Task',
backing, spec=relocate_spec)
LOG.debug(_("Initiated relocation of volume backing: %s.") % backing)
self._session.wait_for_task(task)
LOG.info(_("Successfully relocated volume backing: %(backing)s "
"to datastore: %(ds)s and resource pool: %(rp)s.") %
{'backing': backing, 'ds': datastore, 'rp': resource_pool})
def move_backing_to_folder(self, backing, folder):
"""Move the volume backing to the folder.
:param backing: Reference to the backing
:param folder: Reference to the folder
"""
LOG.debug(_("Moving backing: %(backing)s to folder: %(fol)s.") %
{'backing': backing, 'fol': folder})
task = self._session.invoke_api(self._session.vim,
'MoveIntoFolder_Task', folder,
list=[backing])
LOG.debug(_("Initiated move of volume backing: %(backing)s into the "
"folder: %(fol)s.") % {'backing': backing, 'fol': folder})
self._session.wait_for_task(task)
LOG.info(_("Successfully moved volume backing: %(backing)s into the "
"folder: %(fol)s.") % {'backing': backing, 'fol': folder})
def create_snapshot(self, backing, name, description):
"""Create snapshot of the backing with given name and description.
:param backing: Reference to the backing entity
:param name: Snapshot name
:param description: Snapshot description
:return: Created snapshot entity reference
"""
LOG.debug(_("Snapshoting backing: %(backing)s with name: %(name)s.") %
{'backing': backing, 'name': name})
task = self._session.invoke_api(self._session.vim,
'CreateSnapshot_Task',
backing, name=name,
description=description,
memory=False, quiesce=False)
LOG.debug(_("Initiated snapshot of volume backing: %(backing)s "
"named: %(name)s.") % {'backing': backing, 'name': name})
task_info = self._session.wait_for_task(task)
snapshot = task_info.result
LOG.info(_("Successfully created snapshot: %(snap)s for volume "
"backing: %(backing)s.") %
{'snap': snapshot, 'backing': backing})
return snapshot
@staticmethod
def _get_snapshot_from_tree(name, root):
"""Get snapshot by name from the snapshot tree root.
:param name: Snapshot name
:param root: Current root node in the snapshot tree
:return: None in the snapshot tree with given snapshot name
"""
if not root:
return None
if root.name == name:
return root.snapshot
if (not hasattr(root, 'childSnapshotList') or
not root.childSnapshotList):
# When root does not have children, the childSnapshotList attr
# is missing sometime. Adding an additional check.
return None
for node in root.childSnapshotList:
snapshot = VMwareVolumeOps._get_snapshot_from_tree(name, node)
if snapshot:
return snapshot
def get_snapshot(self, backing, name):
"""Get snapshot of the backing with given name.
:param backing: Reference to the backing entity
:param name: Snapshot name
:return: Snapshot entity of the backing with given name
"""
snapshot = self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, backing,
'snapshot')
if not snapshot or not snapshot.rootSnapshotList:
return None
for root in snapshot.rootSnapshotList:
return VMwareVolumeOps._get_snapshot_from_tree(name, root)
def delete_snapshot(self, backing, name):
"""Delete a given snapshot from volume backing.
:param backing: Reference to the backing entity
:param name: Snapshot name
"""
LOG.debug(_("Deleting the snapshot: %(name)s from backing: "
"%(backing)s.") %
{'name': name, 'backing': backing})
snapshot = self.get_snapshot(backing, name)
if not snapshot:
LOG.info(_("Did not find the snapshot: %(name)s for backing: "
"%(backing)s. Need not delete anything.") %
{'name': name, 'backing': backing})
return
task = self._session.invoke_api(self._session.vim,
'RemoveSnapshot_Task',
snapshot, removeChildren=False)
LOG.debug(_("Initiated snapshot: %(name)s deletion for backing: "
"%(backing)s.") %
{'name': name, 'backing': backing})
self._session.wait_for_task(task)
LOG.info(_("Successfully deleted snapshot: %(name)s of backing: "
"%(backing)s.") % {'backing': backing, 'name': name})
def _get_folder(self, backing):
"""Get parent folder of the backing.
:param backing: Reference to the backing entity
:return: Reference to parent folder of the backing entity
"""
return self._get_parent(backing, 'Folder')
def _get_clone_spec(self, datastore, disk_move_type, snapshot):
"""Get the clone spec.
:param datastore: Reference to datastore
:param disk_move_type: Disk move type
:param snapshot: Reference to snapshot
:return: Clone spec
"""
relocate_spec = self._get_relocate_spec(datastore, None, None,
disk_move_type)
cf = self._session.vim.client.factory
clone_spec = cf.create('ns0:VirtualMachineCloneSpec')
clone_spec.location = relocate_spec
clone_spec.powerOn = False
clone_spec.template = False
clone_spec.snapshot = snapshot
LOG.debug(_("Spec for cloning the backing: %s.") % clone_spec)
return clone_spec
def clone_backing(self, name, backing, snapshot, clone_type, datastore):
"""Clone backing.
If the clone_type is 'full', then a full clone of the source volume
backing will be created. Else, if it is 'linked', then a linked clone
of the source volume backing will be created.
:param name: Name for the clone
:param backing: Reference to the backing entity
:param snapshot: Snapshot point from which the clone should be done
:param clone_type: Whether a full clone or linked clone is to be made
:param datastore: Reference to the datastore entity
"""
LOG.debug(_("Creating a clone of backing: %(back)s, named: %(name)s, "
"clone type: %(type)s from snapshot: %(snap)s on "
"datastore: %(ds)s") %
{'back': backing, 'name': name, 'type': clone_type,
'snap': snapshot, 'ds': datastore})
folder = self._get_folder(backing)
if clone_type == LINKED_CLONE_TYPE:
disk_move_type = 'createNewChildDiskBacking'
else:
disk_move_type = 'moveAllDiskBackingsAndDisallowSharing'
clone_spec = self._get_clone_spec(datastore, disk_move_type, snapshot)
task = self._session.invoke_api(self._session.vim, 'CloneVM_Task',
backing, folder=folder, name=name,
spec=clone_spec)
LOG.debug(_("Initiated clone of backing: %s.") % name)
task_info = self._session.wait_for_task(task)
new_backing = task_info.result
LOG.info(_("Successfully created clone: %s.") % new_backing)
return new_backing
def _delete_file(self, file_path, datacenter=None):
"""Delete file or folder on the datastore.
:param file_path: Datastore path of the file or folder
"""
LOG.debug(_("Deleting file: %(file)s under datacenter: %(dc)s.") %
{'file': file_path, 'dc': datacenter})
fileManager = self._session.vim.service_content.fileManager
task = self._session.invoke_api(self._session.vim,
'DeleteDatastoreFile_Task',
fileManager,
name=file_path,
datacenter=datacenter)
LOG.debug(_("Initiated deletion via task: %s.") % task)
self._session.wait_for_task(task)
LOG.info(_("Successfully deleted file: %s.") % file_path)
def copy_backing(self, src_folder_path, dest_folder_path):
"""Copy the backing folder recursively onto the destination folder.
This method overwrites all the files at the destination if present
by deleting them first.
:param src_folder_path: Datastore path of the source folder
:param dest_folder_path: Datastore path of the destination
"""
LOG.debug(_("Copying backing files from %(src)s to %(dest)s.") %
{'src': src_folder_path, 'dest': dest_folder_path})
fileManager = self._session.vim.service_content.fileManager
try:
task = self._session.invoke_api(self._session.vim,
'CopyDatastoreFile_Task',
fileManager,
sourceName=src_folder_path,
destinationName=dest_folder_path)
LOG.debug(_("Initiated copying of backing via task: %s.") % task)
self._session.wait_for_task(task)
LOG.info(_("Successfully copied backing to %s.") %
dest_folder_path)
except error_util.VimFaultException as excep:
if FILE_ALREADY_EXISTS not in excep.fault_list:
raise excep
# There might be files on datastore due to previous failed attempt
# We clean the folder up and retry the copy
self._delete_file(dest_folder_path)
self.copy_backing(src_folder_path, dest_folder_path)
def get_path_name(self, backing):
"""Get path name of the backing.
:param backing: Reference to the backing entity
:return: Path name of the backing
"""
return self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, backing,
'config.files').vmPathName
def register_backing(self, path, name, folder, resource_pool):
"""Register backing to the inventory.
:param path: Datastore path to the backing
:param name: Name with which we register the backing
:param folder: Reference to the folder entity
:param resource_pool: Reference to the resource pool entity
:return: Reference to the backing that is registered
"""
try:
LOG.debug(_("Registering backing at path: %s to inventory.") %
path)
task = self._session.invoke_api(self._session.vim,
'RegisterVM_Task', folder,
path=path, name=name,
asTemplate=False,
pool=resource_pool)
LOG.debug(_("Initiated registring backing, task: %s.") % task)
task_info = self._session.wait_for_task(task)
backing = task_info.result
LOG.info(_("Successfully registered backing: %s.") % backing)
return backing
except error_util.VimFaultException as excep:
if ALREADY_EXISTS not in excep.fault_list:
raise excep
# If the vmx is already registered to the inventory that may
# happen due to previous failed attempts, then we simply retrieve
# the backing moref based on name and return.
return self.get_backing(name)
def revert_to_snapshot(self, snapshot):
"""Revert backing to a snapshot point.
:param snapshot: Reference to the snapshot entity
"""
LOG.debug(_("Reverting backing to snapshot: %s.") % snapshot)
task = self._session.invoke_api(self._session.vim,
'RevertToSnapshot_Task',
snapshot)
LOG.debug(_("Initiated reverting snapshot via task: %s.") % task)
self._session.wait_for_task(task)
LOG.info(_("Successfully reverted to snapshot: %s.") % snapshot)

View File

@ -1506,6 +1506,40 @@
#storwize_svc_multihostmap_enabled=true
#
# Options defined in cinder.volume.drivers.vmware.vmdk
#
# IP address for connecting to VMware ESX/VC server. (string
# value)
#vmware_host_ip=<None>
# Username for authenticating with VMware ESX/VC server.
# (string value)
#vmware_host_username=<None>
# Password for authenticating with VMware ESX/VC server.
# (string value)
#vmware_host_password=<None>
# Optional VIM service WSDL Location e.g
# http://<server>/vimService.wsdl. Optional over-ride to
# default location for bug work-arounds. (string value)
#vmware_wsdl_location=<None>
# Number of times VMware ESX/VC server API must be retried
# upon connection related issues. (integer value)
#vmware_api_retry_count=10
# The interval used for polling remote tasks invoked on VMware
# ESX/VC server. (integer value)
#vmware_task_poll_interval=5
# Name for the folder in the VC datacenter that will contain
# cinder volumes. (string value)
#vmware_volume_folder=cinder-volumes
#
# Options defined in cinder.volume.drivers.windows
#
@ -1617,4 +1651,4 @@
#volume_dd_blocksize=1M
# Total option count: 346
# Total option count: 353

View File

@ -9,6 +9,7 @@ iso8601>=0.1.4
kombu>=2.4.8
lockfile>=0.8
lxml>=2.3
netaddr
oslo.config>=1.1.0
paramiko>=1.8.0
Paste
@ -21,5 +22,6 @@ six
SQLAlchemy>=0.7.8,<=0.7.99
sqlalchemy-migrate>=0.7.2
stevedore>=0.10
suds>=0.4
WebOb>=1.2.3,<1.3
wsgiref>=0.1.2