Coverage for nova/virt/vmwareapi/images.py: 0%

225 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +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""" 

19 

20import os 

21import tarfile 

22 

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 

31 

32 

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 

39 

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 

46 

47LOG = logging.getLogger(__name__) 

48IMAGE_API = glance.API() 

49 

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 

53 

54 

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. 

67 

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 

87 

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) 

93 

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 

99 

100 @property 

101 def file_size_in_kb(self): 

102 return self.file_size / units.Ki 

103 

104 @property 

105 def is_sparse(self): 

106 return self.disk_type == constants.DISK_TYPE_SPARSE 

107 

108 @property 

109 def is_iso(self): 

110 return self.file_type == constants.DISK_FORMAT_ISO 

111 

112 @property 

113 def is_ova(self): 

114 return self.container_format == constants.CONTAINER_FORMAT_OVA 

115 

116 @classmethod 

117 def from_image(cls, context, image_id, image_meta): 

118 """Returns VMwareImage, the subset of properties the driver uses. 

119 

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 

127 

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) 

132 

133 # catch any string values that need to be interpreted as boolean values 

134 linked_clone = strutils.bool_from_string(image_linked_clone) 

135 

136 if image_meta.obj_attr_is_set('container_format'): 

137 container_format = image_meta.container_format 

138 else: 

139 container_format = None 

140 

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 } 

147 

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) 

169 

170 props_map = { 

171 'os_distro': 'os_type', 

172 'hw_disk_type': 'disk_type', 

173 'hw_vif_model': 'vif_model' 

174 } 

175 

176 for k, v in props_map.items(): 

177 if properties.obj_attr_is_set(k): 

178 props[v] = properties.get(k) 

179 

180 return cls(**props) 

181 

182 

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 

195 

196 

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() 

213 

214 

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) 

227 

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() 

236 

237 LOG.debug("Uploaded iso %s to datastore", iso_path, 

238 instance=instance) 

239 

240 

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) 

250 

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) 

265 

266 

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. 

269 

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. 

273 

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 

288 

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 

307 

308 vm_file_info = cf.create('ns0:VirtualMachineFileInfo') 

309 vm_file_info.vmPathName = '[%s]' % ds_name 

310 

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 

318 

319 return create_spec 

320 

321 

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) 

325 

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 

330 

331 

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) 

340 

341 metadata = IMAGE_API.get(context, image_ref) 

342 file_size = int(metadata['size']) 

343 

344 vm_import_spec = _build_import_spec_for_import_vapp( 

345 session, vm_name, ds_name) 

346 

347 read_iter = IMAGE_API.download(context, image_ref) 

348 read_handle = rw_handles.ImageReadHandle(read_iter) 

349 

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) 

358 

359 imported_vm_ref = write_handle.get_imported_vm() 

360 

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 

367 

368 

369def get_vmdk_name_from_ovf(xmlstr): 

370 """Parse the OVA descriptor to extract the vmdk name.""" 

371 

372 ovf = etree.fromstring(encodeutils.safe_encode(xmlstr)) 

373 nsovf = "{%s}" % ovf.nsmap["ovf"] 

374 

375 disk = ovf.find("./%sDiskSection/%sDisk" % (nsovf, nsovf)) 

376 file_id = disk.get("%sfileRef" % nsovf) 

377 

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 

382 

383 

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) 

394 

395 metadata = IMAGE_API.get(context, image_ref) 

396 file_size = int(metadata['size']) 

397 

398 vm_import_spec = _build_import_spec_for_import_vapp( 

399 session, vm_name, ds_name) 

400 

401 read_iter = IMAGE_API.download(context, image_ref) 

402 read_handle = rw_handles.ImageReadHandle(read_iter) 

403 

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) 

436 

437 

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) 

443 

444 read_handle = rw_handles.VmdkReadHandle(session, 

445 session._host, 

446 session._port, 

447 vm, 

448 None, 

449 vmdk_size) 

450 

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}} 

463 

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() 

471 

472 LOG.debug("Uploaded image %s to the Glance image server", image_id, 

473 instance=instance)