cinder/cinder/volume/drivers/vmware/vmdk.py

1004 lines
42 KiB
Python

# 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 vmware_images
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.'),
cfg.IntOpt('vmware_image_transfer_timeout_secs',
default=7200,
help='Timeout in seconds for VMDK volume transfer between '
'Cinder and Glance.'),
cfg.IntOpt('vmware_max_objects_retrieval',
default=100,
help='Max number of objects to be retrieved per batch. '
'Query results will be obtained in batches from the '
'server and not in one shot. Server may still limit the '
'count to something less than the configured value.'),
]
CONF = cfg.CONF
CONF.register_opts(vmdk_opts)
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:
max_objects = self.configuration.vmware_max_objects_retrieval
self._volumeops = volumeops.VMwareVolumeOps(self.session,
max_objects)
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
max_objects = self.configuration.vmware_max_objects_retrieval
self._volumeops = volumeops.VMwareVolumeOps(self.session, max_objects)
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 accommodate 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 accommodate %(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 _select_ds_for_volume(self, size_gb):
"""Select datastore that can accommodate a volume of given size.
Returns the selected datastore summary along with a compute host and
its resource pool and folder where the volume can be created
:return: (host, rp, folder, summary)
"""
retrv_result = self.volumeops.get_hosts()
while retrv_result:
hosts = retrv_result.objects
if not hosts:
break
(selected_host, rp, folder, summary) = (None, None, None, None)
for host in hosts:
host = host.obj
try:
(dss, rp) = self.volumeops.get_dss_rp(host)
(folder, summary) = self._get_folder_ds_summary(size_gb,
rp, dss)
selected_host = host
break
except error_util.VimException as excep:
LOG.warn(_("Unable to find suitable datastore for volume "
"of size: %(vol)s GB under host: %(host)s. "
"More details: %(excep)s") %
{'vol': size_gb,
'host': host.obj, 'excep': excep})
if selected_host:
self.volumeops.cancel_retrieval(retrv_result)
return (selected_host, rp, folder, summary)
retrv_result = self.volumeops.continue_retrieval(retrv_result)
msg = _("Unable to find host to accommodate a disk of size: %s "
"in the inventory.") % size_gb
LOG.error(msg)
raise error_util.VimException(msg)
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
"""
retrv_result = self.volumeops.get_hosts()
while retrv_result:
hosts = retrv_result.objects
if not hosts:
break
backing = None
for host in hosts:
try:
backing = self._create_backing(volume, host.obj)
if backing:
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.obj, 'excep': excep})
if backing:
self.volumeops.cancel_retrieval(retrv_result)
return backing
retrv_result = self.volumeops.continue_retrieval(retrv_result)
msg = _("Unable to create volume: %s in the inventory.")
LOG.error(msg % volume['name'])
raise error_util.VimException(msg % volume['name'])
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.
Snapshot of only available volume is supported.
:param snapshot: Snapshot object
"""
volume = snapshot['volume']
if volume['status'] != 'available':
msg = _("Snapshot of volume not supported in state: %s.")
LOG.error(msg % volume['status'])
raise exception.InvalidVolume(msg % volume['status'])
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.
Snapshot deletion of only available volume is supported.
:param snapshot: Snapshot object
"""
volume = snapshot['volume']
if volume['status'] != 'available':
msg = _("Delete snapshot of volume not supported in state: %s.")
LOG.error(msg % volume['status'])
raise exception.InvalidVolume(msg % volume['status'])
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, src_vmdk_path):
"""Clones volume backing.
Creates a backing for the input volume and replaces its VMDK file
with the input VMDK file copy.
:param volume: New Volume object
:param src_vmdk_path: VMDK file path of the source volume backing
"""
# Create a backing
backing = self._create_backing_in_inventory(volume)
new_vmdk_path = self.volumeops.get_vmdk_path(backing)
datacenter = self.volumeops.get_dc(backing)
# Deleting the current VMDK file
self.volumeops.delete_vmdk_file(new_vmdk_path, datacenter)
# Copying the source VMDK file
self.volumeops.copy_vmdk_file(datacenter, src_vmdk_path, new_vmdk_path)
LOG.info(_("Successfully cloned new backing: %(back)s from "
"source VMDK file: %(vmdk)s.") %
{'back': backing, 'vmdk': src_vmdk_path})
def _create_cloned_volume(self, volume, src_vref):
"""Creates volume clone.
If source volume's backing does not exist, then pass.
Creates a backing and replaces its VMDK file with a copy of the
source backing's VMDK file.
: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
src_vmdk_path = self.volumeops.get_vmdk_path(backing)
self._clone_backing_by_copying(volume, src_vmdk_path)
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 creates clone of source volume backing by copying its VMDK file.
: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
src_vmdk_path = self.volumeops.get_vmdk_path(snapshot_moref)
self._clone_backing_by_copying(volume, src_vmdk_path)
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)
def _get_ds_name_flat_vmdk_path(self, backing, vol_name):
"""Get datastore name and folder path of the flat VMDK of the backing.
:param backing: Reference to the backing entity
:param vol_name: Name of the volume
:return: datastore name and folder path of the VMDK of the backing
"""
file_path_name = self.volumeops.get_path_name(backing)
(datastore_name,
folder_path, _) = volumeops.split_datastore_path(file_path_name)
flat_vmdk_path = '%s%s-flat.vmdk' % (folder_path, vol_name)
return (datastore_name, flat_vmdk_path)
@staticmethod
def _validate_disk_format(disk_format):
"""Verify vmdk as disk format.
:param disk_format: Disk format of the image
"""
if disk_format and disk_format.lower() != 'vmdk':
msg = _("Cannot create image of disk format: %s. Only vmdk "
"disk format is accepted.") % disk_format
LOG.error(msg)
raise exception.ImageUnacceptable(msg)
def _fetch_flat_image(self, context, volume, image_service, image_id,
image_size):
"""Creates a volume from flat glance image.
Creates a backing for the volume under the ESX/VC server and
copies the VMDK flat file from the glance image content.
The method assumes glance image is VMDK disk format and its
vmware_disktype is "sparse" or "preallocated", but not
"streamOptimized"
"""
# Set volume size in GB from image metadata
volume['size'] = float(image_size) / units.GiB
# First create empty backing in the inventory
backing = self._create_backing_in_inventory(volume)
try:
(datastore_name,
flat_vmdk_path) = self._get_ds_name_flat_vmdk_path(backing,
volume['name'])
host = self.volumeops.get_host(backing)
datacenter = self.volumeops.get_dc(host)
datacenter_name = self.volumeops.get_entity_name(datacenter)
flat_vmdk_ds_path = '[%s] %s' % (datastore_name, flat_vmdk_path)
# Delete the *-flat.vmdk file within the backing
self.volumeops.delete_file(flat_vmdk_ds_path, datacenter)
# copy over image from glance into *-flat.vmdk
timeout = self.configuration.vmware_image_transfer_timeout_secs
host_ip = self.configuration.vmware_host_ip
cookies = self.session.vim.client.options.transport.cookiejar
LOG.debug(_("Fetching glance image: %(id)s to server: %(host)s.") %
{'id': image_id, 'host': host_ip})
vmware_images.fetch_flat_image(context, timeout, image_service,
image_id, image_size=image_size,
host=host_ip,
data_center_name=datacenter_name,
datastore_name=datastore_name,
cookies=cookies,
file_path=flat_vmdk_path)
LOG.info(_("Done copying image: %(id)s to volume: %(vol)s.") %
{'id': image_id, 'vol': volume['name']})
except Exception as excep:
LOG.exception(_("Exception in copy_image_to_volume: %(excep)s. "
"Deleting the backing: %(back)s.") %
{'excep': excep, 'back': backing})
# delete the backing
self.volumeops.delete_backing(backing)
raise excep
def _fetch_stream_optimized_image(self, context, volume, image_service,
image_id, image_size):
"""Creates volume from image using HttpNfc VM import.
Uses Nfc API to download the VMDK file from Glance. Nfc creates the
backing VM that wraps the VMDK in the ESX/VC inventory.
This method assumes glance image is VMDK disk format and its
vmware_disktype is 'streamOptimized'.
"""
try:
# find host in which to create the volume
size_gb = volume['size']
(host, rp, folder, summary) = self._select_ds_for_volume(size_gb)
except error_util.VimException as excep:
LOG.exception(_("Exception in _select_ds_for_volume: %s.") % excep)
raise excep
LOG.debug(_("Selected datastore %(ds)s for new volume of size "
"%(size)s GB.") % {'ds': summary.name, 'size': size_gb})
# prepare create spec for backing vm
disk_type = VMwareEsxVmdkDriver._get_disk_type(volume)
# The size of stream optimized glance image is often suspect,
# so better let VC figure out the disk capacity during import.
dummy_disk_size = 0
vm_create_spec = self.volumeops._get_create_spec(volume['name'],
dummy_disk_size,
disk_type,
summary.name)
# convert vm_create_spec to vm_import_spec
cf = self.session.vim.client.factory
vm_import_spec = cf.create('ns0:VirtualMachineImportSpec')
vm_import_spec.configSpec = vm_create_spec
try:
# fetching image from glance will also create the backing
timeout = self.configuration.vmware_image_transfer_timeout_secs
host_ip = self.configuration.vmware_host_ip
LOG.debug(_("Fetching glance image: %(id)s to server: %(host)s.") %
{'id': image_id, 'host': host_ip})
vmware_images.fetch_stream_optimized_image(context, timeout,
image_service,
image_id,
session=self.session,
host=host_ip,
resource_pool=rp,
vm_folder=folder,
vm_create_spec=
vm_import_spec,
image_size=image_size)
except exception.CinderException as excep:
LOG.exception(_("Exception in copy_image_to_volume: %s.") % excep)
backing = self.volumeops.get_backing(volume['name'])
if backing:
LOG.exception(_("Deleting the backing: %s") % backing)
# delete the backing
self.volumeops.delete_backing(backing)
raise excep
LOG.info(_("Done copying image: %(id)s to volume: %(vol)s.") %
{'id': image_id, 'vol': volume['name']})
def copy_image_to_volume(self, context, volume, image_service, image_id):
"""Creates volume from image.
This method only supports Glance image of VMDK disk format.
Uses flat vmdk file copy for "sparse" and "preallocated" disk types
Uses HttpNfc import API for "streamOptimized" disk types. This API
creates a backing VM that wraps the VMDK in the ESX/VC inventory.
:param context: context
:param volume: Volume object
:param image_service: Glance image service
:param image_id: Glance image id
"""
LOG.debug(_("Copy glance image: %s to create new volume.") % image_id)
# Verify glance image is vmdk disk format
metadata = image_service.show(context, image_id)
VMwareEsxVmdkDriver._validate_disk_format(metadata['disk_format'])
# Get disk_type for vmdk disk
disk_type = None
properties = metadata['properties']
if properties and 'vmware_disktype' in properties:
disk_type = properties['vmware_disktype']
if disk_type == 'streamOptimized':
self._fetch_stream_optimized_image(context, volume, image_service,
image_id, metadata['size'])
else:
self._fetch_flat_image(context, volume, image_service, image_id,
metadata['size'])
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Creates glance image from volume.
Upload of only available volume is supported. The uploaded glance image
has a vmdk disk type of "streamOptimized" that can only be downloaded
using the HttpNfc API.
Steps followed are:
1. Get the name of the vmdk file which the volume points to right now.
Can be a chain of snapshots, so we need to know the last in the
chain.
2. Use Nfc APIs to upload the contents of the vmdk file to glance.
"""
# if volume is attached raise exception
if volume['instance_uuid'] or volume['attached_host']:
msg = _("Upload to glance of attached volume is not supported.")
LOG.error(msg)
raise exception.InvalidVolume(msg)
# validate disk format is vmdk
LOG.debug(_("Copy Volume: %s to new image.") % volume['name'])
VMwareEsxVmdkDriver._validate_disk_format(image_meta['disk_format'])
# get backing vm of volume and its vmdk path
backing = self.volumeops.get_backing(volume['name'])
if not backing:
LOG.info(_("Backing not found, creating for volume: %s") %
volume['name'])
backing = self._create_backing_in_inventory(volume)
vmdk_file_path = self.volumeops.get_vmdk_path(backing)
# Upload image from vmdk
timeout = self.configuration.vmware_image_transfer_timeout_secs
host_ip = self.configuration.vmware_host_ip
vmware_images.upload_image(context, timeout, image_service,
image_meta['id'],
volume['project_id'],
session=self.session,
host=host_ip,
vm=backing,
vmdk_file_path=vmdk_file_path,
vmdk_size=volume['size'] * units.GiB,
image_name=image_meta['name'],
image_version=1)
LOG.info(_("Done copying volume %(vol)s to a new image %(img)s") %
{'vol': volume['name'], 'img': image_meta['name']})
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.
Linked clone of attached volume is not supported.
: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:
if src_vref['status'] != 'available':
msg = _("Linked clone of source volume not supported "
"in state: %s.")
LOG.error(msg % src_vref['status'])
raise exception.InvalidVolume(msg % src_vref['status'])
# 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)