Coverage for nova/virt/vmwareapi/images.py: 0%
225 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright (c) 2012 VMware, Inc.
2# Copyright (c) 2011 Citrix Systems, Inc.
3# Copyright 2011 OpenStack Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16"""
17Utility functions for Image transfer and manipulation.
18"""
20import os
21import tarfile
23from lxml import etree
24from oslo_config import cfg
25from oslo_log import log as logging
26from oslo_service import loopingcall
27from oslo_utils import encodeutils
28from oslo_utils import strutils
29from oslo_utils import units
30from oslo_vmware import rw_handles
33from nova import exception
34from nova.i18n import _
35from nova.image import glance
36from nova.objects import fields
37from nova.virt.vmwareapi import constants
38from nova.virt.vmwareapi import vm_util
40# NOTE(mdbooth): We use use_linked_clone below, but don't have to import it
41# because nova.virt.vmwareapi.driver is imported first. In fact, it is not
42# possible to import it here, as nova.virt.vmwareapi.driver calls
43# CONF.register_opts() after the import chain which imports this module. This
44# is not a problem as long as the import order doesn't change.
45CONF = cfg.CONF
47LOG = logging.getLogger(__name__)
48IMAGE_API = glance.API()
50QUEUE_BUFFER_SIZE = 10
51NFC_LEASE_UPDATE_PERIOD = 60 # update NFC lease every 60sec.
52CHUNK_SIZE = 64 * units.Ki # default chunk size for image transfer
55class VMwareImage(object):
56 def __init__(self, image_id,
57 file_size=0,
58 os_type=constants.DEFAULT_OS_TYPE,
59 adapter_type=constants.DEFAULT_ADAPTER_TYPE,
60 disk_type=constants.DEFAULT_DISK_TYPE,
61 container_format=constants.CONTAINER_FORMAT_BARE,
62 file_type=constants.DEFAULT_DISK_FORMAT,
63 linked_clone=None,
64 vsphere_location=None,
65 vif_model=constants.DEFAULT_VIF_MODEL):
66 """VMwareImage holds values for use in building VMs.
68 image_id (str): uuid of the image
69 file_size (int): size of file in bytes
70 os_type (str): name of guest os (use vSphere names only)
71 adapter_type (str): name of the adapter's type
72 disk_type (str): type of disk in thin, thick, etc
73 container_format (str): container format (bare or ova)
74 file_type (str): vmdk or iso
75 linked_clone (bool): use linked clone, or don't
76 vsphere_location (str): image location in datastore or None
77 vif_model (str): virtual machine network interface
78 """
79 self.image_id = image_id
80 self.file_size = file_size
81 self.os_type = os_type
82 self.adapter_type = adapter_type
83 self.container_format = container_format
84 self.disk_type = disk_type
85 self.file_type = file_type
86 self.vsphere_location = vsphere_location
88 # NOTE(vui): This should be removed when we restore the
89 # descriptor-based validation.
90 if (self.file_type is not None and
91 self.file_type not in constants.DISK_FORMATS_ALL):
92 raise exception.InvalidDiskFormat(disk_format=self.file_type)
94 if linked_clone is not None:
95 self.linked_clone = linked_clone
96 else:
97 self.linked_clone = CONF.vmware.use_linked_clone
98 self.vif_model = vif_model
100 @property
101 def file_size_in_kb(self):
102 return self.file_size / units.Ki
104 @property
105 def is_sparse(self):
106 return self.disk_type == constants.DISK_TYPE_SPARSE
108 @property
109 def is_iso(self):
110 return self.file_type == constants.DISK_FORMAT_ISO
112 @property
113 def is_ova(self):
114 return self.container_format == constants.CONTAINER_FORMAT_OVA
116 @classmethod
117 def from_image(cls, context, image_id, image_meta):
118 """Returns VMwareImage, the subset of properties the driver uses.
120 :param context - context
121 :param image_id - image id of image
122 :param image_meta - image metadata object we are working with
123 :return: vmware image object
124 :rtype: nova.virt.vmwareapi.images.VmwareImage
125 """
126 properties = image_meta.properties
128 # calculate linked_clone flag, allow image properties to override the
129 # global property set in the configurations.
130 image_linked_clone = properties.get('img_linked_clone',
131 CONF.vmware.use_linked_clone)
133 # catch any string values that need to be interpreted as boolean values
134 linked_clone = strutils.bool_from_string(image_linked_clone)
136 if image_meta.obj_attr_is_set('container_format'):
137 container_format = image_meta.container_format
138 else:
139 container_format = None
141 props = {
142 'image_id': image_id,
143 'linked_clone': linked_clone,
144 'container_format': container_format,
145 'vsphere_location': get_vsphere_location(context, image_id)
146 }
148 if image_meta.obj_attr_is_set('size'):
149 props['file_size'] = image_meta.size
150 if image_meta.obj_attr_is_set('disk_format'):
151 props['file_type'] = image_meta.disk_format
152 hw_disk_bus = properties.get('hw_disk_bus')
153 if hw_disk_bus:
154 mapping = {
155 fields.SCSIModel.LSILOGIC:
156 constants.DEFAULT_ADAPTER_TYPE,
157 fields.SCSIModel.LSISAS1068:
158 constants.ADAPTER_TYPE_LSILOGICSAS,
159 fields.SCSIModel.BUSLOGIC:
160 constants.ADAPTER_TYPE_BUSLOGIC,
161 fields.SCSIModel.VMPVSCSI:
162 constants.ADAPTER_TYPE_PARAVIRTUAL,
163 }
164 if hw_disk_bus == fields.DiskBus.IDE:
165 props['adapter_type'] = constants.ADAPTER_TYPE_IDE
166 elif hw_disk_bus == fields.DiskBus.SCSI:
167 hw_scsi_model = properties.get('hw_scsi_model')
168 props['adapter_type'] = mapping.get(hw_scsi_model)
170 props_map = {
171 'os_distro': 'os_type',
172 'hw_disk_type': 'disk_type',
173 'hw_vif_model': 'vif_model'
174 }
176 for k, v in props_map.items():
177 if properties.obj_attr_is_set(k):
178 props[v] = properties.get(k)
180 return cls(**props)
183def get_vsphere_location(context, image_id):
184 """Get image location in vsphere or None."""
185 # image_id can be None if the instance is booted using a volume.
186 if image_id:
187 metadata = IMAGE_API.get(context, image_id, include_locations=True)
188 locations = metadata.get('locations')
189 if locations:
190 for loc in locations:
191 loc_url = loc.get('url')
192 if loc_url and loc_url.startswith('vsphere://'):
193 return loc_url
194 return None
197def image_transfer(read_handle, write_handle):
198 # write_handle could be an NFC lease, so we need to periodically
199 # update its progress
200 update_cb = getattr(write_handle, 'update_progress', lambda: None)
201 updater = loopingcall.FixedIntervalLoopingCall(update_cb)
202 try:
203 updater.start(interval=NFC_LEASE_UPDATE_PERIOD)
204 while True:
205 data = read_handle.read(CHUNK_SIZE)
206 if not data:
207 break
208 write_handle.write(data)
209 finally:
210 updater.stop()
211 read_handle.close()
212 write_handle.close()
215def upload_iso_to_datastore(iso_path, instance, **kwargs):
216 LOG.debug("Uploading iso %s to datastore", iso_path,
217 instance=instance)
218 with open(iso_path, 'r') as iso_file:
219 write_file_handle = rw_handles.FileWriteHandle(
220 kwargs.get("host"),
221 kwargs.get("port"),
222 kwargs.get("data_center_name"),
223 kwargs.get("datastore_name"),
224 kwargs.get("cookies"),
225 kwargs.get("file_path"),
226 os.fstat(iso_file.fileno()).st_size)
228 LOG.debug("Uploading iso of size : %s ",
229 os.fstat(iso_file.fileno()).st_size)
230 block_size = 0x10000
231 data = iso_file.read(block_size)
232 while len(data) > 0:
233 write_file_handle.write(data)
234 data = iso_file.read(block_size)
235 write_file_handle.close()
237 LOG.debug("Uploaded iso %s to datastore", iso_path,
238 instance=instance)
241def fetch_image(context, instance, host, port, dc_name, ds_name, file_path,
242 cookies=None):
243 """Download image from the glance image server."""
244 image_ref = instance.image_ref
245 LOG.debug("Downloading image file data %(image_ref)s to the "
246 "data store %(data_store_name)s",
247 {'image_ref': image_ref,
248 'data_store_name': ds_name},
249 instance=instance)
251 metadata = IMAGE_API.get(context, image_ref)
252 file_size = int(metadata['size'])
253 read_iter = IMAGE_API.download(context, image_ref)
254 read_file_handle = rw_handles.ImageReadHandle(read_iter)
255 write_file_handle = rw_handles.FileWriteHandle(
256 host, port, dc_name, ds_name, cookies, file_path, file_size)
257 image_transfer(read_file_handle, write_file_handle)
258 LOG.debug("Downloaded image file data %(image_ref)s to "
259 "%(upload_name)s on the data store "
260 "%(data_store_name)s",
261 {'image_ref': image_ref,
262 'upload_name': 'n/a' if file_path is None else file_path,
263 'data_store_name': 'n/a' if ds_name is None else ds_name},
264 instance=instance)
267def _build_shadow_vm_config_spec(session, name, size_kb, disk_type, ds_name):
268 """Return spec for creating a shadow VM for image disk.
270 The VM is never meant to be powered on. When used in importing
271 a disk it governs the directory name created for the VM
272 and the disk type of the disk image to convert to.
274 :param name: Name of the backing
275 :param size_kb: Size in KB of the backing
276 :param disk_type: VMDK type for the disk
277 :param ds_name: Datastore name where the disk is to be provisioned
278 :return: Spec for creation
279 """
280 cf = session.vim.client.factory
281 controller_device = cf.create('ns0:VirtualLsiLogicController')
282 controller_device.key = -100
283 controller_device.busNumber = 0
284 controller_device.sharedBus = 'noSharing'
285 controller_spec = cf.create('ns0:VirtualDeviceConfigSpec')
286 controller_spec.operation = 'add'
287 controller_spec.device = controller_device
289 disk_device = cf.create('ns0:VirtualDisk')
290 # for very small disks allocate at least 1KB
291 disk_device.capacityInKB = max(1, int(size_kb))
292 disk_device.key = -101
293 disk_device.unitNumber = 0
294 disk_device.controllerKey = -100
295 disk_device_bkng = cf.create('ns0:VirtualDiskFlatVer2BackingInfo')
296 if disk_type == constants.DISK_TYPE_EAGER_ZEROED_THICK:
297 disk_device_bkng.eagerlyScrub = True
298 elif disk_type == constants.DISK_TYPE_THIN:
299 disk_device_bkng.thinProvisioned = True
300 disk_device_bkng.fileName = '[%s]' % ds_name
301 disk_device_bkng.diskMode = 'persistent'
302 disk_device.backing = disk_device_bkng
303 disk_spec = cf.create('ns0:VirtualDeviceConfigSpec')
304 disk_spec.operation = 'add'
305 disk_spec.fileOperation = 'create'
306 disk_spec.device = disk_device
308 vm_file_info = cf.create('ns0:VirtualMachineFileInfo')
309 vm_file_info.vmPathName = '[%s]' % ds_name
311 create_spec = cf.create('ns0:VirtualMachineConfigSpec')
312 create_spec.name = name
313 create_spec.guestId = constants.DEFAULT_OS_TYPE
314 create_spec.numCPUs = 1
315 create_spec.memoryMB = 128
316 create_spec.deviceChange = [controller_spec, disk_spec]
317 create_spec.files = vm_file_info
319 return create_spec
322def _build_import_spec_for_import_vapp(session, vm_name, datastore_name):
323 vm_create_spec = _build_shadow_vm_config_spec(
324 session, vm_name, 0, constants.DISK_TYPE_THIN, datastore_name)
326 client_factory = session.vim.client.factory
327 vm_import_spec = client_factory.create('ns0:VirtualMachineImportSpec')
328 vm_import_spec.configSpec = vm_create_spec
329 return vm_import_spec
332def fetch_image_stream_optimized(context, instance, session, vm_name,
333 ds_name, vm_folder_ref, res_pool_ref):
334 """Fetch image from Glance to ESX datastore."""
335 image_ref = instance.image_ref
336 LOG.debug("Downloading image file data %(image_ref)s to the ESX "
337 "as VM named '%(vm_name)s'",
338 {'image_ref': image_ref, 'vm_name': vm_name},
339 instance=instance)
341 metadata = IMAGE_API.get(context, image_ref)
342 file_size = int(metadata['size'])
344 vm_import_spec = _build_import_spec_for_import_vapp(
345 session, vm_name, ds_name)
347 read_iter = IMAGE_API.download(context, image_ref)
348 read_handle = rw_handles.ImageReadHandle(read_iter)
350 write_handle = rw_handles.VmdkWriteHandle(session,
351 session._host,
352 session._port,
353 res_pool_ref,
354 vm_folder_ref,
355 vm_import_spec,
356 file_size)
357 image_transfer(read_handle, write_handle)
359 imported_vm_ref = write_handle.get_imported_vm()
361 LOG.info("Downloaded image file data %(image_ref)s",
362 {'image_ref': instance.image_ref}, instance=instance)
363 vmdk = vm_util.get_vmdk_info(session, imported_vm_ref)
364 session._call_method(session.vim, "UnregisterVM", imported_vm_ref)
365 LOG.info("The imported VM was unregistered", instance=instance)
366 return vmdk.capacity_in_bytes
369def get_vmdk_name_from_ovf(xmlstr):
370 """Parse the OVA descriptor to extract the vmdk name."""
372 ovf = etree.fromstring(encodeutils.safe_encode(xmlstr))
373 nsovf = "{%s}" % ovf.nsmap["ovf"]
375 disk = ovf.find("./%sDiskSection/%sDisk" % (nsovf, nsovf))
376 file_id = disk.get("%sfileRef" % nsovf)
378 file = ovf.find('./%sReferences/%sFile[@%sid="%s"]' % (nsovf, nsovf,
379 nsovf, file_id))
380 vmdk_name = file.get("%shref" % nsovf)
381 return vmdk_name
384def fetch_image_ova(context, instance, session, vm_name, ds_name,
385 vm_folder_ref, res_pool_ref):
386 """Download the OVA image from the glance image server to the
387 Nova compute node.
388 """
389 image_ref = instance.image_ref
390 LOG.debug("Downloading OVA image file %(image_ref)s to the ESX "
391 "as VM named '%(vm_name)s'",
392 {'image_ref': image_ref, 'vm_name': vm_name},
393 instance=instance)
395 metadata = IMAGE_API.get(context, image_ref)
396 file_size = int(metadata['size'])
398 vm_import_spec = _build_import_spec_for_import_vapp(
399 session, vm_name, ds_name)
401 read_iter = IMAGE_API.download(context, image_ref)
402 read_handle = rw_handles.ImageReadHandle(read_iter)
404 with tarfile.open(mode="r|", fileobj=read_handle) as tar:
405 vmdk_name = None
406 for tar_info in tar:
407 if tar_info and tar_info.name.endswith(".ovf"):
408 extracted = tar.extractfile(tar_info)
409 xmlstr = extracted.read()
410 vmdk_name = get_vmdk_name_from_ovf(xmlstr)
411 elif vmdk_name and tar_info.name.startswith(vmdk_name):
412 # Actual file name is <vmdk_name>.XXXXXXX
413 extracted = tar.extractfile(tar_info)
414 write_handle = rw_handles.VmdkWriteHandle(
415 session,
416 session._host,
417 session._port,
418 res_pool_ref,
419 vm_folder_ref,
420 vm_import_spec,
421 file_size)
422 image_transfer(extracted, write_handle)
423 LOG.info("Downloaded OVA image file %(image_ref)s",
424 {'image_ref': instance.image_ref}, instance=instance)
425 imported_vm_ref = write_handle.get_imported_vm()
426 vmdk = vm_util.get_vmdk_info(session,
427 imported_vm_ref)
428 session._call_method(session.vim, "UnregisterVM",
429 imported_vm_ref)
430 LOG.info("The imported VM was unregistered",
431 instance=instance)
432 return vmdk.capacity_in_bytes
433 raise exception.ImageUnacceptable(
434 reason=_("Extracting vmdk from OVA failed."),
435 image_id=image_ref)
438def upload_image_stream_optimized(context, image_id, instance, session,
439 vm, vmdk_size):
440 """Upload the snapshotted vm disk file to Glance image server."""
441 LOG.debug("Uploading image %s", image_id, instance=instance)
442 metadata = IMAGE_API.get(context, image_id)
444 read_handle = rw_handles.VmdkReadHandle(session,
445 session._host,
446 session._port,
447 vm,
448 None,
449 vmdk_size)
451 # Set the image properties. It is important to set the 'size' to 0.
452 # Otherwise, the image service client will use the VM's disk capacity
453 # which will not be the image size after upload, since it is converted
454 # to a stream-optimized sparse disk.
455 image_metadata = {'disk_format': constants.DISK_FORMAT_VMDK,
456 'name': metadata['name'],
457 'status': 'active',
458 'container_format': constants.CONTAINER_FORMAT_BARE,
459 'size': 0,
460 'properties': {'vmware_image_version': 1,
461 'vmware_disktype': 'streamOptimized',
462 'owner_id': instance.project_id}}
464 updater = loopingcall.FixedIntervalLoopingCall(read_handle.update_progress)
465 try:
466 updater.start(interval=NFC_LEASE_UPDATE_PERIOD)
467 IMAGE_API.update(context, image_id, image_metadata, data=read_handle)
468 finally:
469 updater.stop()
470 read_handle.close()
472 LOG.debug("Uploaded image %s to the Glance image server", image_id,
473 instance=instance)