| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you 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. |
| |
| package com.cloud.hypervisor.kvm.storage; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| import org.apache.cloudstack.storage.datastore.util.ScaleIOUtil; |
| import org.apache.cloudstack.utils.cryptsetup.CryptSetup; |
| import org.apache.cloudstack.utils.cryptsetup.CryptSetupException; |
| import org.apache.cloudstack.utils.cryptsetup.KeyFile; |
| import org.apache.cloudstack.utils.qemu.QemuImageOptions; |
| import org.apache.cloudstack.utils.qemu.QemuImg; |
| import org.apache.cloudstack.utils.qemu.QemuImgException; |
| import org.apache.cloudstack.utils.qemu.QemuImgFile; |
| import org.apache.cloudstack.utils.qemu.QemuObject; |
| import org.apache.commons.io.filefilter.WildcardFileFilter; |
| import org.apache.log4j.Logger; |
| import org.libvirt.LibvirtException; |
| |
| import com.cloud.storage.Storage; |
| import com.cloud.storage.StorageLayer; |
| import com.cloud.storage.StorageManager; |
| import com.cloud.utils.exception.CloudRuntimeException; |
| import com.cloud.utils.script.OutputInterpreter; |
| import com.cloud.utils.script.Script; |
| import org.apache.commons.lang3.StringUtils; |
| |
| @StorageAdaptorInfo(storagePoolType= Storage.StoragePoolType.PowerFlex) |
| public class ScaleIOStorageAdaptor implements StorageAdaptor { |
| private static final Logger LOGGER = Logger.getLogger(ScaleIOStorageAdaptor.class); |
| private static final Map<String, KVMStoragePool> MapStorageUuidToStoragePool = new HashMap<>(); |
| private static final int DEFAULT_DISK_WAIT_TIME_IN_SECS = 60; |
| private StorageLayer storageLayer; |
| |
| public ScaleIOStorageAdaptor(StorageLayer storagelayer) { |
| storageLayer = storagelayer; |
| } |
| |
| @Override |
| public KVMStoragePool getStoragePool(String uuid) { |
| KVMStoragePool pool = MapStorageUuidToStoragePool.get(uuid); |
| if (pool == null) { |
| LOGGER.error("Pool: " + uuid + " not found, probably sdc not connected on agent start"); |
| throw new CloudRuntimeException("Pool: " + uuid + " not found, reconnect sdc and restart agent if sdc not connected on agent start"); |
| } |
| |
| return pool; |
| } |
| |
| @Override |
| public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { |
| return getStoragePool(uuid); |
| } |
| |
| @Override |
| public KVMPhysicalDisk getPhysicalDisk(String volumePath, KVMStoragePool pool) { |
| if (StringUtils.isEmpty(volumePath) || pool == null) { |
| LOGGER.error("Unable to get physical disk, volume path or pool not specified"); |
| return null; |
| } |
| |
| String volumeId = ScaleIOUtil.getVolumePath(volumePath); |
| |
| try { |
| String diskFilePath = null; |
| String systemId = ScaleIOUtil.getSystemIdForVolume(volumeId); |
| if (StringUtils.isNotEmpty(systemId) && systemId.length() == ScaleIOUtil.IDENTIFIER_LENGTH) { |
| // Disk path format: /dev/disk/by-id/emc-vol-<SystemID>-<VolumeID> |
| final String diskFileName = ScaleIOUtil.DISK_NAME_PREFIX + systemId + "-" + volumeId; |
| diskFilePath = ScaleIOUtil.DISK_PATH + File.separator + diskFileName; |
| final File diskFile = new File(diskFilePath); |
| if (!diskFile.exists()) { |
| LOGGER.debug("Physical disk file: " + diskFilePath + " doesn't exists on the storage pool: " + pool.getUuid()); |
| return null; |
| } |
| } else { |
| LOGGER.debug("Try with wildcard filter to get the physical disk: " + volumeId + " on the storage pool: " + pool.getUuid()); |
| final File dir = new File(ScaleIOUtil.DISK_PATH); |
| final FileFilter fileFilter = new WildcardFileFilter(ScaleIOUtil.DISK_NAME_PREFIX_FILTER + volumeId); |
| final File[] files = dir.listFiles(fileFilter); |
| if (files != null && files.length == 1) { |
| diskFilePath = files[0].getAbsolutePath(); |
| } else { |
| LOGGER.debug("Unable to find the physical disk: " + volumeId + " on the storage pool: " + pool.getUuid()); |
| return null; |
| } |
| } |
| |
| KVMPhysicalDisk disk = new KVMPhysicalDisk(diskFilePath, volumePath, pool); |
| |
| // try to discover format as written to disk, rather than assuming raw. |
| // We support qcow2 for stored primary templates, disks seen as other should be treated as raw. |
| QemuImg qemu = new QemuImg(0); |
| QemuImgFile qemuFile = new QemuImgFile(diskFilePath); |
| Map<String, String> details = qemu.info(qemuFile); |
| String detectedFormat = details.getOrDefault(QemuImg.FILE_FORMAT, "none"); |
| if (detectedFormat.equalsIgnoreCase(QemuImg.PhysicalDiskFormat.QCOW2.toString())) { |
| disk.setFormat(QemuImg.PhysicalDiskFormat.QCOW2); |
| } else { |
| disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); |
| } |
| |
| long diskSize = getPhysicalDiskSize(diskFilePath); |
| disk.setSize(diskSize); |
| |
| if (details.containsKey(QemuImg.VIRTUAL_SIZE)) { |
| disk.setVirtualSize(Long.parseLong(details.get(QemuImg.VIRTUAL_SIZE))); |
| } else { |
| disk.setVirtualSize(diskSize); |
| } |
| |
| return disk; |
| } catch (Exception e) { |
| LOGGER.error("Failed to get the physical disk: " + volumePath + " on the storage pool: " + pool.getUuid() + " due to " + e.getMessage()); |
| throw new CloudRuntimeException("Failed to get the physical disk: " + volumePath + " on the storage pool: " + pool.getUuid()); |
| } |
| } |
| |
| @Override |
| public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, Storage.StoragePoolType type, Map<String, String> details) { |
| ScaleIOStoragePool storagePool = new ScaleIOStoragePool(uuid, host, port, path, type, details, this); |
| MapStorageUuidToStoragePool.put(uuid, storagePool); |
| return storagePool; |
| } |
| |
| @Override |
| public boolean deleteStoragePool(String uuid) { |
| return MapStorageUuidToStoragePool.remove(uuid) != null; |
| } |
| |
| /** |
| * ScaleIO doesn't need to communicate with the hypervisor normally to create a volume. This is used only to prepare a ScaleIO data disk for encryption. |
| * Thin encrypted volumes are provisioned in QCOW2 format, which insulates the guest from zeroes/unallocated blocks in the block device that would |
| * otherwise show up as garbage data through the encryption layer. As a bonus, encrypted QCOW2 format handles discard. |
| * @param name disk path |
| * @param pool pool |
| * @param format disk format |
| * @param provisioningType provisioning type |
| * @param size disk size |
| * @param passphrase passphrase |
| * @return the disk object |
| */ |
| @Override |
| public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { |
| if (passphrase == null || passphrase.length == 0) { |
| return null; |
| } |
| |
| if(!connectPhysicalDisk(name, pool, null)) { |
| throw new CloudRuntimeException(String.format("Failed to ensure disk %s was present", name)); |
| } |
| |
| KVMPhysicalDisk disk = getPhysicalDisk(name, pool); |
| |
| if (provisioningType.equals(Storage.ProvisioningType.THIN)) { |
| disk.setFormat(QemuImg.PhysicalDiskFormat.QCOW2); |
| disk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); |
| try (KeyFile keyFile = new KeyFile(passphrase)){ |
| QemuImg qemuImg = new QemuImg(0, true, false); |
| Map<String, String> options = new HashMap<>(); |
| List<QemuObject> qemuObjects = new ArrayList<>(); |
| long formattedSize = getUsableBytesFromRawBytes(disk.getSize()); |
| |
| options.put("preallocation", QemuImg.PreallocationType.Metadata.toString()); |
| qemuObjects.add(QemuObject.prepareSecretForQemuImg(disk.getFormat(), disk.getQemuEncryptFormat(), keyFile.toString(), "sec0", options)); |
| QemuImgFile file = new QemuImgFile(disk.getPath(), formattedSize, disk.getFormat()); |
| qemuImg.create(file, null, options, qemuObjects); |
| LOGGER.debug(String.format("Successfully formatted %s as encrypted QCOW2", file.getFileName())); |
| } catch (QemuImgException | LibvirtException | IOException ex) { |
| throw new CloudRuntimeException("Failed to set up encrypted QCOW on block device " + disk.getPath(), ex); |
| } |
| } else { |
| try { |
| CryptSetup crypt = new CryptSetup(); |
| crypt.luksFormat(passphrase, CryptSetup.LuksType.LUKS, disk.getPath()); |
| disk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); |
| disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); |
| } catch (CryptSetupException ex) { |
| throw new CloudRuntimeException("Failed to set up encryption for block device " + disk.getPath(), ex); |
| } |
| } |
| |
| return disk; |
| } |
| |
| @Override |
| public boolean connectPhysicalDisk(String volumePath, KVMStoragePool pool, Map<String, String> details) { |
| if (StringUtils.isEmpty(volumePath) || pool == null) { |
| LOGGER.error("Unable to connect physical disk due to insufficient data"); |
| throw new CloudRuntimeException("Unable to connect physical disk due to insufficient data"); |
| } |
| |
| volumePath = ScaleIOUtil.getVolumePath(volumePath); |
| |
| int waitTimeInSec = DEFAULT_DISK_WAIT_TIME_IN_SECS; |
| if (details != null && details.containsKey(StorageManager.STORAGE_POOL_DISK_WAIT.toString())) { |
| String waitTime = details.get(StorageManager.STORAGE_POOL_DISK_WAIT.toString()); |
| if (StringUtils.isNotEmpty(waitTime)) { |
| waitTimeInSec = Integer.valueOf(waitTime).intValue(); |
| } |
| } |
| return waitForDiskToBecomeAvailable(volumePath, pool, waitTimeInSec); |
| } |
| |
| private boolean waitForDiskToBecomeAvailable(String volumePath, KVMStoragePool pool, int waitTimeInSec) { |
| LOGGER.debug("Waiting for the volume with id: " + volumePath + " of the storage pool: " + pool.getUuid() + " to become available for " + waitTimeInSec + " secs"); |
| int timeBetweenTries = 1000; // Try more frequently (every sec) and return early if disk is found |
| KVMPhysicalDisk physicalDisk = null; |
| |
| // Rescan before checking for the physical disk |
| ScaleIOUtil.rescanForNewVolumes(); |
| |
| while (waitTimeInSec > 0) { |
| physicalDisk = getPhysicalDisk(volumePath, pool); |
| if (physicalDisk != null && physicalDisk.getSize() > 0) { |
| LOGGER.debug("Found the volume with id: " + volumePath + " of the storage pool: " + pool.getUuid()); |
| return true; |
| } |
| |
| waitTimeInSec--; |
| |
| try { |
| Thread.sleep(timeBetweenTries); |
| } catch (Exception ex) { |
| // don't do anything |
| } |
| } |
| |
| physicalDisk = getPhysicalDisk(volumePath, pool); |
| if (physicalDisk != null && physicalDisk.getSize() > 0) { |
| LOGGER.debug("Found the volume using id: " + volumePath + " of the storage pool: " + pool.getUuid()); |
| return true; |
| } |
| |
| LOGGER.debug("Unable to find the volume with id: " + volumePath + " of the storage pool: " + pool.getUuid()); |
| return false; |
| } |
| |
| private long getPhysicalDiskSize(String diskPath) { |
| if (StringUtils.isEmpty(diskPath)) { |
| return 0; |
| } |
| |
| Script diskCmd = new Script("blockdev", LOGGER); |
| diskCmd.add("--getsize64", diskPath); |
| |
| OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); |
| String result = diskCmd.execute(parser); |
| |
| if (result != null) { |
| LOGGER.warn("Unable to get the disk size at path: " + diskPath); |
| return 0; |
| } else { |
| LOGGER.info("Able to retrieve the disk size at path:" + diskPath); |
| } |
| |
| return Long.parseLong(parser.getLine()); |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDisk(String volumePath, KVMStoragePool pool) { |
| return true; |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDisk(Map<String, String> volumeToDisconnect) { |
| return false; |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDiskByPath(String localPath) { |
| return false; |
| } |
| |
| @Override |
| public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.ImageFormat format) { |
| return true; |
| } |
| |
| @Override |
| public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String name, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, KVMStoragePool destPool, int timeout, byte[] passphrase) { |
| return null; |
| } |
| |
| @Override |
| public KVMPhysicalDisk createTemplateFromDisk(KVMPhysicalDisk disk, String name, QemuImg.PhysicalDiskFormat format, long size, KVMStoragePool destPool) { |
| return null; |
| } |
| |
| @Override |
| public List<KVMPhysicalDisk> listPhysicalDisks(String storagePoolUuid, KVMStoragePool pool) { |
| return null; |
| } |
| |
| @Override |
| public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout) { |
| return copyPhysicalDisk(disk, name, destPool, timeout, null, null, Storage.ProvisioningType.THIN); |
| } |
| |
| @Override |
| public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout, byte[] srcPassphrase, byte[]dstPassphrase, Storage.ProvisioningType provisioningType) { |
| if (StringUtils.isEmpty(name) || disk == null || destPool == null) { |
| LOGGER.error("Unable to copy physical disk due to insufficient data"); |
| throw new CloudRuntimeException("Unable to copy physical disk due to insufficient data"); |
| } |
| |
| if (provisioningType == null) { |
| provisioningType = Storage.ProvisioningType.THIN; |
| } |
| |
| LOGGER.debug("Copy physical disk with size: " + disk.getSize() + ", virtualsize: " + disk.getVirtualSize()+ ", format: " + disk.getFormat()); |
| |
| KVMPhysicalDisk destDisk = destPool.getPhysicalDisk(name); |
| if (destDisk == null) { |
| LOGGER.error("Failed to find the disk: " + name + " of the storage pool: " + destPool.getUuid()); |
| throw new CloudRuntimeException("Failed to find the disk: " + name + " of the storage pool: " + destPool.getUuid()); |
| } |
| |
| destDisk.setVirtualSize(disk.getVirtualSize()); |
| destDisk.setSize(disk.getSize()); |
| |
| QemuImg qemu = null; |
| QemuImgFile srcQemuFile = null; |
| QemuImgFile destQemuFile = null; |
| String srcKeyName = "sec0"; |
| String destKeyName = "sec1"; |
| List<QemuObject> qemuObjects = new ArrayList<>(); |
| Map<String, String> options = new HashMap<String, String>(); |
| CryptSetup cryptSetup = null; |
| |
| try (KeyFile srcKey = new KeyFile(srcPassphrase); KeyFile dstKey = new KeyFile(dstPassphrase)){ |
| qemu = new QemuImg(timeout, provisioningType.equals(Storage.ProvisioningType.FAT), false); |
| String srcPath = disk.getPath(); |
| String destPath = destDisk.getPath(); |
| |
| QemuImageOptions qemuImageOpts = new QemuImageOptions(srcPath); |
| |
| srcQemuFile = new QemuImgFile(srcPath, disk.getFormat()); |
| destQemuFile = new QemuImgFile(destPath); |
| |
| if (disk.useAsTemplate()) { |
| destQemuFile.setFormat(QemuImg.PhysicalDiskFormat.QCOW2); |
| } |
| |
| if (srcKey.isSet()) { |
| qemuObjects.add(QemuObject.prepareSecretForQemuImg(disk.getFormat(), disk.getQemuEncryptFormat(), srcKey.toString(), srcKeyName, options)); |
| qemuImageOpts = new QemuImageOptions(disk.getFormat(), srcPath, srcKeyName); |
| } |
| |
| if (dstKey.isSet()) { |
| if (!provisioningType.equals(Storage.ProvisioningType.FAT)) { |
| destDisk.setFormat(QemuImg.PhysicalDiskFormat.QCOW2); |
| destQemuFile.setFormat(QemuImg.PhysicalDiskFormat.QCOW2); |
| options.put("preallocation", QemuImg.PreallocationType.Metadata.toString()); |
| } else { |
| qemu.setSkipZero(false); |
| destDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW); |
| // qemu-img wants to treat RAW + encrypt formatting as LUKS |
| destQemuFile.setFormat(QemuImg.PhysicalDiskFormat.LUKS); |
| } |
| qemuObjects.add(QemuObject.prepareSecretForQemuImg(destDisk.getFormat(), QemuObject.EncryptFormat.LUKS, dstKey.toString(), destKeyName, options)); |
| destDisk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); |
| } |
| |
| boolean forceSourceFormat = srcQemuFile.getFormat() == QemuImg.PhysicalDiskFormat.RAW; |
| LOGGER.debug(String.format("Starting copy from source disk %s(%s) to PowerFlex volume %s(%s), forcing source format is %b", srcQemuFile.getFileName(), srcQemuFile.getFormat(), destQemuFile.getFileName(), destQemuFile.getFormat(), forceSourceFormat)); |
| qemuImageOpts.setImageOptsFlag(true); |
| qemu.convert(srcQemuFile, destQemuFile, options, qemuObjects, qemuImageOpts,null, forceSourceFormat); |
| LOGGER.debug("Successfully converted source disk image " + srcQemuFile.getFileName() + " to PowerFlex volume: " + destDisk.getPath()); |
| |
| if (destQemuFile.getFormat() == QemuImg.PhysicalDiskFormat.QCOW2 && !disk.useAsTemplate()) { |
| QemuImageOptions resizeOptions = new QemuImageOptions(destQemuFile.getFormat(), destPath, destKeyName); |
| resizeQcow2ToVolume(destPath, resizeOptions, qemuObjects, timeout); |
| LOGGER.debug("Resized volume at " + destPath); |
| } |
| } catch (QemuImgException | LibvirtException | IOException e) { |
| try { |
| Map<String, String> srcInfo = qemu.info(srcQemuFile); |
| LOGGER.debug("Source disk info: " + Arrays.asList(srcInfo)); |
| } catch (Exception ignored) { |
| LOGGER.warn("Unable to get info from source disk: " + disk.getName()); |
| } |
| |
| String errMsg = String.format("Unable to convert/copy from %s to %s, due to: %s", disk.getName(), name, ((StringUtils.isEmpty(e.getMessage())) ? "an unknown error" : e.getMessage())); |
| LOGGER.error(errMsg); |
| throw new CloudRuntimeException(errMsg, e); |
| } finally { |
| if (cryptSetup != null) { |
| try { |
| cryptSetup.close(name); |
| } catch (CryptSetupException ex) { |
| LOGGER.warn("Failed to clean up LUKS disk after copying disk", ex); |
| } |
| } |
| } |
| |
| return destDisk; |
| } |
| |
| |
| @Override |
| public boolean refresh(KVMStoragePool pool) { |
| return true; |
| } |
| |
| @Override |
| public boolean deleteStoragePool(KVMStoragePool pool) { |
| return deleteStoragePool(pool.getUuid()); |
| } |
| |
| @Override |
| public boolean createFolder(String uuid, String path) { |
| return createFolder(uuid, path, null); |
| } |
| |
| @Override |
| public boolean createFolder(String uuid, String path, String localPath) { |
| return true; |
| } |
| |
| |
| @Override |
| public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk template, String name, QemuImg.PhysicalDiskFormat format, long size, KVMStoragePool destPool, int timeout, byte[] passphrase) { |
| return null; |
| } |
| |
| @Override |
| public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFilePath, String destTemplatePath, KVMStoragePool destPool, Storage.ImageFormat format, int timeout) { |
| if (StringUtils.isAnyEmpty(templateFilePath, destTemplatePath) || destPool == null) { |
| LOGGER.error("Unable to create template from direct download template file due to insufficient data"); |
| throw new CloudRuntimeException("Unable to create template from direct download template file due to insufficient data"); |
| } |
| |
| LOGGER.debug("Create template from direct download template - file path: " + templateFilePath + ", dest path: " + destTemplatePath + ", format: " + format.toString()); |
| |
| File sourceFile = new File(templateFilePath); |
| if (!sourceFile.exists()) { |
| throw new CloudRuntimeException("Direct download template file " + templateFilePath + " does not exist on this host"); |
| } |
| |
| if (destTemplatePath == null || destTemplatePath.isEmpty()) { |
| LOGGER.error("Failed to create template, target template disk path not provided"); |
| throw new CloudRuntimeException("Target template disk path not provided"); |
| } |
| |
| if (destPool.getType() != Storage.StoragePoolType.PowerFlex) { |
| throw new CloudRuntimeException("Unsupported storage pool type: " + destPool.getType().toString()); |
| } |
| |
| if (Storage.ImageFormat.RAW.equals(format) && Storage.ImageFormat.QCOW2.equals(format)) { |
| LOGGER.error("Failed to create template, unsupported template format: " + format.toString()); |
| throw new CloudRuntimeException("Unsupported template format: " + format.toString()); |
| } |
| |
| String srcTemplateFilePath = templateFilePath; |
| KVMPhysicalDisk destDisk = null; |
| QemuImgFile srcFile = null; |
| QemuImgFile destFile = null; |
| try { |
| QemuImg qemu = new QemuImg(timeout, true, false); |
| destDisk = destPool.getPhysicalDisk(destTemplatePath); |
| if (destDisk == null) { |
| LOGGER.error("Failed to find the disk: " + destTemplatePath + " of the storage pool: " + destPool.getUuid()); |
| throw new CloudRuntimeException("Failed to find the disk: " + destTemplatePath + " of the storage pool: " + destPool.getUuid()); |
| } |
| |
| if (isTemplateExtractable(templateFilePath)) { |
| srcTemplateFilePath = sourceFile.getParent() + "/" + UUID.randomUUID().toString(); |
| LOGGER.debug("Extract the downloaded template " + templateFilePath + " to " + srcTemplateFilePath); |
| String extractCommand = getExtractCommandForDownloadedFile(templateFilePath, srcTemplateFilePath); |
| Script.runSimpleBashScript(extractCommand); |
| Script.runSimpleBashScript("rm -f " + templateFilePath); |
| } |
| |
| QemuImg.PhysicalDiskFormat srcFileFormat = QemuImg.PhysicalDiskFormat.RAW; |
| if (format == Storage.ImageFormat.RAW) { |
| srcFileFormat = QemuImg.PhysicalDiskFormat.RAW; |
| } else if (format == Storage.ImageFormat.QCOW2) { |
| srcFileFormat = QemuImg.PhysicalDiskFormat.QCOW2; |
| } |
| |
| srcFile = new QemuImgFile(srcTemplateFilePath, srcFileFormat); |
| qemu.info(srcFile); |
| /** |
| * Even though the disk itself is raw, we store templates on ScaleIO in qcow2 format. |
| * This improves performance by reading/writing less data to volume, saves the unused space for encryption header, and |
| * nicely encapsulates VM images that might contain LUKS data (as opposed to converting to raw which would look like a LUKS volume). |
| */ |
| destFile = new QemuImgFile(destDisk.getPath(), QemuImg.PhysicalDiskFormat.QCOW2); |
| destFile.setSize(srcFile.getSize()); |
| |
| LOGGER.debug("Starting copy from source downloaded template " + srcFile.getFileName() + " to PowerFlex template volume: " + destDisk.getPath()); |
| qemu.create(destFile); |
| qemu.convert(srcFile, destFile); |
| LOGGER.debug("Successfully converted source downloaded template " + srcFile.getFileName() + " to PowerFlex template volume: " + destDisk.getPath()); |
| } catch (QemuImgException | LibvirtException e) { |
| LOGGER.error("Failed to convert. The error was: " + e.getMessage(), e); |
| destDisk = null; |
| } finally { |
| Script.runSimpleBashScript("rm -f " + srcTemplateFilePath); |
| } |
| |
| return destDisk; |
| } |
| |
| private boolean isTemplateExtractable(String templatePath) { |
| String type = Script.runSimpleBashScript("file " + templatePath + " | awk -F' ' '{print $2}'"); |
| return type.equalsIgnoreCase("bzip2") || type.equalsIgnoreCase("gzip") || type.equalsIgnoreCase("zip"); |
| } |
| |
| private String getExtractCommandForDownloadedFile(String downloadedTemplateFile, String templateFile) { |
| if (downloadedTemplateFile.endsWith(".zip")) { |
| return "unzip -p " + downloadedTemplateFile + " | cat > " + templateFile; |
| } else if (downloadedTemplateFile.endsWith(".bz2")) { |
| return "bunzip2 -c " + downloadedTemplateFile + " > " + templateFile; |
| } else if (downloadedTemplateFile.endsWith(".gz")) { |
| return "gunzip -c " + downloadedTemplateFile + " > " + templateFile; |
| } else { |
| throw new CloudRuntimeException("Unable to extract template " + downloadedTemplateFile); |
| } |
| } |
| |
| public void resizeQcow2ToVolume(String volumePath, QemuImageOptions options, List<QemuObject> objects, Integer timeout) throws QemuImgException, LibvirtException { |
| long rawSizeBytes = getPhysicalDiskSize(volumePath); |
| long usableSizeBytes = getUsableBytesFromRawBytes(rawSizeBytes); |
| QemuImg qemu = new QemuImg(timeout); |
| qemu.resize(options, objects, usableSizeBytes); |
| } |
| |
| /** |
| * Calculates usable size from raw size, assuming qcow2 requires 192k/1GB for metadata |
| * We also remove 128MiB for encryption/fragmentation/safety factor. |
| * @param raw size in bytes |
| * @return usable size in bytesbytes |
| */ |
| public static long getUsableBytesFromRawBytes(Long raw) { |
| long usable = raw - (128 << 20) - ((raw >> 30) * 200704); |
| if (usable < 0) { |
| usable = 0L; |
| } |
| return usable; |
| } |
| } |