| // 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.BufferedReader; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.UUID; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.apache.cloudstack.utils.qemu.QemuImg; |
| import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; |
| import org.apache.cloudstack.utils.qemu.QemuImgException; |
| import org.apache.cloudstack.utils.qemu.QemuImgFile; |
| |
| import com.cloud.storage.Storage; |
| import com.cloud.storage.StorageManager; |
| import com.cloud.utils.PropertiesUtil; |
| import com.cloud.utils.exception.CloudRuntimeException; |
| import com.cloud.utils.script.OutputInterpreter; |
| import com.cloud.utils.script.Script; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.libvirt.LibvirtException; |
| import org.joda.time.Duration; |
| |
| public abstract class MultipathSCSIAdapterBase implements StorageAdaptor { |
| protected static Logger LOGGER = LogManager.getLogger(MultipathSCSIAdapterBase.class); |
| static final Map<String, KVMStoragePool> MapStorageUuidToStoragePool = new HashMap<>(); |
| |
| /** |
| * A lock to avoid any possiblity of multiple requests for a scan |
| */ |
| static byte[] CLEANUP_LOCK = new byte[0]; |
| |
| /** |
| * Property keys and defaults |
| */ |
| static final Property<Integer> CLEANUP_FREQUENCY_SECS = new Property<Integer>("multimap.cleanup.frequency.secs", 60); |
| static final Property<Integer> CLEANUP_TIMEOUT_SECS = new Property<Integer>("multimap.cleanup.timeout.secs", 4); |
| static final Property<Boolean> CLEANUP_ENABLED = new Property<Boolean>("multimap.cleanup.enabled", true); |
| static final Property<String> CLEANUP_SCRIPT = new Property<String>("multimap.cleanup.script", "cleanStaleMaps.sh"); |
| static final Property<String> CONNECT_SCRIPT = new Property<String>("multimap.connect.script", "connectVolume.sh"); |
| static final Property<String> COPY_SCRIPT = new Property<String>("multimap.copy.script", "copyVolume.sh"); |
| static final Property<String> DISCONNECT_SCRIPT = new Property<String>("multimap.disconnect.script", "disconnectVolume.sh"); |
| static final Property<String> RESIZE_SCRIPT = new Property<String>("multimap.resize.script", "resizeVolume.sh"); |
| static final Property<Integer> DISK_WAIT_SECS = new Property<Integer>("multimap.disk.wait.secs", 240); |
| static final Property<String> STORAGE_SCRIPTS_DIR = new Property<String>("multimap.storage.scripts.dir", "scripts/storage/multipath"); |
| |
| static Timer cleanupTimer = new Timer(); |
| private static int cleanupTimeoutSecs = CLEANUP_TIMEOUT_SECS.getFinalValue(); |
| private static String connectScript = CONNECT_SCRIPT.getFinalValue(); |
| private static String disconnectScript = DISCONNECT_SCRIPT.getFinalValue(); |
| private static String cleanupScript = CLEANUP_SCRIPT.getFinalValue(); |
| private static String resizeScript = RESIZE_SCRIPT.getFinalValue(); |
| private static String copyScript = COPY_SCRIPT.getFinalValue(); |
| private static int diskWaitTimeSecs = DISK_WAIT_SECS.getFinalValue(); |
| |
| /** |
| * Initialize static program-wide configurations and background jobs |
| */ |
| static { |
| long cleanupFrequency = CLEANUP_FREQUENCY_SECS.getFinalValue() * 1000; |
| boolean cleanupEnabled = CLEANUP_ENABLED.getFinalValue(); |
| |
| |
| connectScript = Script.findScript(STORAGE_SCRIPTS_DIR.getFinalValue(), connectScript); |
| if (connectScript == null) { |
| throw new Error("Unable to find the connectVolume.sh script"); |
| } |
| |
| disconnectScript = Script.findScript(STORAGE_SCRIPTS_DIR.getFinalValue(), disconnectScript); |
| if (disconnectScript == null) { |
| throw new Error("Unable to find the disconnectVolume.sh script"); |
| } |
| |
| resizeScript = Script.findScript(STORAGE_SCRIPTS_DIR.getFinalValue(), resizeScript); |
| if (resizeScript == null) { |
| throw new Error("Unable to find the resizeVolume.sh script"); |
| } |
| |
| copyScript = Script.findScript(STORAGE_SCRIPTS_DIR.getFinalValue(), copyScript); |
| if (copyScript == null) { |
| throw new Error("Unable to find the copyVolume.sh script"); |
| } |
| |
| if (cleanupEnabled) { |
| cleanupScript = Script.findScript(STORAGE_SCRIPTS_DIR.getFinalValue(), cleanupScript); |
| if (cleanupScript == null) { |
| throw new Error("Unable to find the cleanStaleMaps.sh script and " + CLEANUP_ENABLED.getName() + " is true"); |
| } |
| |
| TimerTask task = new TimerTask() { |
| @Override |
| public void run() { |
| try { |
| MultipathSCSIAdapterBase.cleanupStaleMaps(); |
| } catch (Throwable e) { |
| LOGGER.warn("Error running stale multipath map cleanup", e); |
| } |
| } |
| }; |
| |
| cleanupTimer = new Timer("MultipathMapCleanupJob"); |
| cleanupTimer.scheduleAtFixedRate(task, 0, cleanupFrequency); |
| } |
| } |
| |
| @Override |
| public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { |
| return getStoragePool(uuid); |
| } |
| |
| public abstract String getName(); |
| |
| public abstract boolean isStoragePoolTypeSupported(Storage.StoragePoolType type); |
| |
| /** |
| * We expect WWN values in the volumePath so need to convert it to an actual physical path |
| */ |
| public abstract AddressInfo parseAndValidatePath(String path); |
| |
| @Override |
| public KVMPhysicalDisk getPhysicalDisk(String volumePath, KVMStoragePool pool) { |
| LOGGER.debug(String.format("getPhysicalDisk(volumePath,pool) called with args (%s,%s)", volumePath, pool)); |
| |
| if (StringUtils.isEmpty(volumePath) || pool == null) { |
| LOGGER.error("Unable to get physical disk, volume path or pool not specified"); |
| return null; |
| } |
| |
| AddressInfo address = parseAndValidatePath(volumePath); |
| return getPhysicalDisk(address, pool); |
| } |
| |
| private KVMPhysicalDisk getPhysicalDisk(AddressInfo address, KVMStoragePool pool) { |
| LOGGER.debug(String.format("getPhysicalDisk(addressInfo,pool) called with args (%s,%s)", address.getPath(), pool)); |
| KVMPhysicalDisk disk = new KVMPhysicalDisk(address.getPath(), address.toString(), pool); |
| disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); |
| |
| long diskSize = getPhysicalDiskSize(address.getPath()); |
| disk.setSize(diskSize); |
| disk.setVirtualSize(diskSize); |
| LOGGER.debug("Physical disk " + disk.getPath() + " with format " + disk.getFormat() + " and size " + disk.getSize() + " provided"); |
| return disk; |
| } |
| |
| @Override |
| public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, Storage.StoragePoolType type, Map<String, String> details) { |
| LOGGER.info(String.format("createStoragePool(uuid,host,port,path,type) called with args (%s, %s, %s, %s, %s)", uuid, host, ""+port, path, type)); |
| MultipathSCSIPool storagePool = new MultipathSCSIPool(uuid, host, port, path, type, details, this); |
| MapStorageUuidToStoragePool.put(uuid, storagePool); |
| return storagePool; |
| } |
| |
| @Override |
| public boolean deleteStoragePool(String uuid) { |
| return MapStorageUuidToStoragePool.remove(uuid) != null; |
| } |
| |
| @Override |
| public boolean connectPhysicalDisk(String volumePath, KVMStoragePool pool, Map<String, String> details) { |
| LOGGER.info("connectPhysicalDisk called for [" + volumePath + "]"); |
| |
| if (StringUtils.isEmpty(volumePath)) { |
| LOGGER.error("Unable to connect physical disk due to insufficient data - volume path is undefined"); |
| throw new CloudRuntimeException("Unable to connect physical disk due to insufficient data - volume path is underfined"); |
| } |
| |
| if (pool == null) { |
| LOGGER.error("Unable to connect physical disk due to insufficient data - pool is not set"); |
| throw new CloudRuntimeException("Unable to connect physical disk due to insufficient data - pool is not set"); |
| } |
| |
| AddressInfo address = this.parseAndValidatePath(volumePath); |
| int waitTimeInSec = diskWaitTimeSecs; |
| 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(address, pool, waitTimeInSec); |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDisk(String volumePath, KVMStoragePool pool) { |
| LOGGER.debug(String.format("disconnectPhysicalDiskByPath(volumePath,pool) called with args (%s, %s) START", volumePath, pool.getUuid())); |
| AddressInfo address = this.parseAndValidatePath(volumePath); |
| ScriptResult result = runScript(disconnectScript, 60000L, address.getAddress().toLowerCase()); |
| if (LOGGER.isDebugEnabled()) LOGGER.debug("multipath flush output: " + result.getResult()); |
| LOGGER.debug(String.format("disconnectPhysicalDiskByPath(volumePath,pool) called with args (%s, %s) COMPLETE [rc=%s]", volumePath, pool.getUuid(), result.getResult())); return true; |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDisk(Map<String, String> volumeToDisconnect) { |
| LOGGER.debug(String.format("disconnectPhysicalDiskByPath(volumeToDisconnect) called with arg bag [not implemented]:") + " " + volumeToDisconnect); |
| return false; |
| } |
| |
| @Override |
| public boolean disconnectPhysicalDiskByPath(String localPath) { |
| LOGGER.debug(String.format("disconnectPhysicalDiskByPath(localPath) called with args (%s) STARTED", localPath)); |
| ScriptResult result = runScript(disconnectScript, 60000L, localPath.replace("/dev/mapper/3", "")); |
| if (LOGGER.isDebugEnabled()) LOGGER.debug("multipath flush output: " + result.getResult()); |
| LOGGER.debug(String.format("disconnectPhysicalDiskByPath(localPath) called with args (%s) COMPLETE [rc=%s]", localPath, result.getExitCode())); return true; |
| } |
| |
| @Override |
| public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.ImageFormat format) { |
| LOGGER.info(String.format("deletePhysicalDisk(uuid,pool,format) called with args (%s, %s, %s) [not implemented]", uuid, pool.getUuid(), format.toString())); |
| return true; |
| } |
| |
| @Override |
| public KVMPhysicalDisk createTemplateFromDisk(KVMPhysicalDisk disk, String name, QemuImg.PhysicalDiskFormat format, long size, KVMStoragePool destPool) { |
| LOGGER.info(String.format("createTemplateFromDisk(disk,name,format,size,destPool) called with args (%s, %s, %s, %s, %s) [not implemented]", disk.getPath(), name, format.toString(), ""+size, destPool.getUuid())); |
| return null; |
| } |
| |
| @Override |
| public List<KVMPhysicalDisk> listPhysicalDisks(String storagePoolUuid, KVMStoragePool pool) { |
| LOGGER.info(String.format("listPhysicalDisks(uuid,pool) called with args (%s, %s) [not implemented]", storagePoolUuid, pool.getUuid())); |
| return null; |
| } |
| |
| @Override |
| public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout) { |
| return copyPhysicalDisk(disk, name, destPool, timeout, null, null, null); |
| } |
| |
| @Override |
| public boolean refresh(KVMStoragePool pool) { |
| LOGGER.info(String.format("refresh(pool) called with args (%s)", pool.getUuid())); |
| return true; |
| } |
| |
| @Override |
| public boolean deleteStoragePool(KVMStoragePool pool) { |
| LOGGER.info(String.format("deleteStroagePool(pool) called with args (%s)", pool.getUuid())); |
| return deleteStoragePool(pool.getUuid()); |
| } |
| |
| @Override |
| public boolean createFolder(String uuid, String path) { |
| LOGGER.info(String.format("createFolder(uuid,path) called with args (%s, %s) [not implemented]", uuid, path)); |
| return createFolder(uuid, path, null); |
| } |
| |
| @Override |
| public boolean createFolder(String uuid, String path, String localPath) { |
| LOGGER.info(String.format("createFolder(uuid,path,localPath) called with args (%s, %s, %s) [not implemented]", uuid, path, localPath)); |
| return true; |
| } |
| |
| /** |
| * Validate inputs and return the source file for a template copy |
| * @param templateFilePath |
| * @param destTemplatePath |
| * @param destPool |
| * @param format |
| * @return |
| */ |
| File createTemplateFromDirectDownloadFileValidate(String templateFilePath, String destTemplatePath, KVMStoragePool destPool, Storage.ImageFormat format) { |
| 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 (this.isStoragePoolTypeSupported(destPool.getType())) { |
| 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()); |
| } |
| return sourceFile; |
| } |
| |
| String extractSourceTemplateIfNeeded(File sourceFile, String templateFilePath) { |
| String srcTemplateFilePath = templateFilePath; |
| 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); |
| } |
| return srcTemplateFilePath; |
| } |
| |
| QemuImg.PhysicalDiskFormat deriveImgFileFormat(Storage.ImageFormat format) { |
| if (format == Storage.ImageFormat.RAW) { |
| return QemuImg.PhysicalDiskFormat.RAW; |
| } else if (format == Storage.ImageFormat.QCOW2) { |
| return QemuImg.PhysicalDiskFormat.QCOW2; |
| } else { |
| return QemuImg.PhysicalDiskFormat.RAW; |
| } |
| } |
| |
| @Override |
| public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFilePath, String destTemplatePath, KVMStoragePool destPool, Storage.ImageFormat format, int timeout) { |
| File sourceFile = createTemplateFromDirectDownloadFileValidate(templateFilePath, destTemplatePath, destPool, format); |
| LOGGER.debug("Create template from direct download template - file path: " + templateFilePath + ", dest path: " + destTemplatePath + ", format: " + format.toString()); |
| KVMPhysicalDisk sourceDisk = destPool.getPhysicalDisk(sourceFile.getAbsolutePath()); |
| return copyPhysicalDisk(sourceDisk, destTemplatePath, 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) { |
| |
| validateForDiskCopy(disk, name, destPool); |
| LOGGER.info("Copying FROM source physical disk " + disk.getPath() + ", 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()); |
| } |
| |
| if (srcPassphrase != null || dstPassphrase != null) { |
| throw new CloudRuntimeException("Storage provider does not support user-space encrypted source or destination volumes"); |
| } |
| |
| destDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW); |
| destDisk.setVirtualSize(disk.getVirtualSize()); |
| destDisk.setSize(disk.getSize()); |
| |
| LOGGER.info("Copying TO destination physical disk " + destDisk.getPath() + ", size: " + destDisk.getSize() + ", virtualsize: " + destDisk.getVirtualSize()+ ", format: " + destDisk.getFormat()); |
| QemuImgFile srcFile = new QemuImgFile(disk.getPath(), disk.getFormat()); |
| QemuImgFile destFile = new QemuImgFile(destDisk.getPath(), destDisk.getFormat()); |
| LOGGER.debug("Starting COPY from source downloaded template " + srcFile.getFileName() + " to Primera volume: " + destDisk.getPath()); |
| ScriptResult result = runScript(copyScript, timeout, destDisk.getFormat().toString().toLowerCase(), srcFile.getFileName(), destFile.getFileName()); |
| int rc = result.getExitCode(); |
| if (rc != 0) { |
| throw new CloudRuntimeException("Failed to convert from " + srcFile.getFileName() + " to " + destFile.getFileName() + " the error was: " + rc + " - " + result.getResult()); |
| } |
| LOGGER.debug("Successfully converted source downloaded template " + srcFile.getFileName() + " to Primera volume: " + destDisk.getPath() + " " + result.getResult()); |
| |
| return destDisk; |
| } |
| |
| void validateForDiskCopy(KVMPhysicalDisk disk, String name, KVMStoragePool destPool) { |
| 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"); |
| } |
| } |
| |
| /** |
| * Copy a disk path to another disk path using QemuImg command |
| * @param disk |
| * @param destDisk |
| * @param name |
| * @param timeout |
| */ |
| void qemuCopy(KVMPhysicalDisk disk, KVMPhysicalDisk destDisk, String name, int timeout) { |
| QemuImg qemu; |
| try { |
| qemu = new QemuImg(timeout); |
| } catch (LibvirtException | QemuImgException e) { |
| throw new CloudRuntimeException (e); |
| } |
| QemuImgFile srcFile = null; |
| QemuImgFile destFile = null; |
| |
| try { |
| srcFile = new QemuImgFile(disk.getPath(), disk.getFormat()); |
| destFile = new QemuImgFile(destDisk.getPath(), destDisk.getFormat()); |
| |
| LOGGER.debug("Starting copy from source disk image " + srcFile.getFileName() + " to volume: " + destDisk.getPath()); |
| qemu.convert(srcFile, destFile, true); |
| LOGGER.debug("Successfully converted source disk image " + srcFile.getFileName() + " to volume: " + destDisk.getPath()); |
| } catch (QemuImgException | LibvirtException e) { |
| try { |
| Map<String, String> srcInfo = qemu.info(srcFile); |
| 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); |
| } |
| } |
| |
| @Override |
| public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, |
| String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, |
| KVMStoragePool destPool, int timeout, byte[] passphrase) { |
| throw new UnsupportedOperationException("Unimplemented method 'createDiskFromTemplate'"); |
| } |
| |
| @Override |
| public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk template, |
| String name, PhysicalDiskFormat format, long size, |
| KVMStoragePool destPool, int timeout, byte[] passphrase) { |
| throw new UnsupportedOperationException("Unimplemented method 'createDiskFromTemplateBacking'"); |
| } |
| |
| @Override |
| public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, |
| PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { |
| throw new UnsupportedOperationException("Unimplemented method 'createPhysicalDisk'"); |
| } |
| |
| boolean isTemplateExtractable(String templatePath) { |
| ScriptResult result = runScript("file", 5000L, templatePath, "| awk -F' ' '{print $2}'"); |
| String type = result.getResult(); |
| return type.equalsIgnoreCase("bzip2") || type.equalsIgnoreCase("gzip") || type.equalsIgnoreCase("zip"); |
| } |
| |
| 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); |
| } |
| } |
| |
| private static final ScriptResult runScript(String script, long timeout, String...args) { |
| ScriptResult result = new ScriptResult(); |
| Script cmd = new Script(script, Duration.millis(timeout), LOGGER); |
| cmd.add(args); |
| OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); |
| String output = cmd.execute(parser); |
| // its possible the process never launches which causes an NPE on getExitValue below |
| if (output != null && output.contains("Unable to execute the command")) { |
| result.setResult(output); |
| result.setExitCode(-1); |
| return result; |
| } |
| result.setResult(output); |
| result.setExitCode(cmd.getExitValue()); |
| return result; |
| } |
| |
| boolean waitForDiskToBecomeAvailable(AddressInfo address, KVMStoragePool pool, long waitTimeInSec) { |
| LOGGER.debug("Waiting for the volume with id: " + address.getPath() + " of the storage pool: " + pool.getUuid() + " to become available for " + waitTimeInSec + " secs"); |
| long scriptTimeoutSecs = 30; // how long to wait for each script execution to run |
| long maxTries = 10; // how many max retries to attempt the script |
| long waitTimeInMillis = waitTimeInSec * 1000; // how long overall to wait |
| int timeBetweenTries = 1000; // how long to sleep between tries |
| // wait at least 60 seconds even if input was lower |
| if (waitTimeInSec < 60) { |
| waitTimeInSec = 60; |
| } |
| KVMPhysicalDisk physicalDisk = null; |
| |
| // Rescan before checking for the physical disk |
| int tries = 0; |
| while (waitTimeInMillis > 0 && tries < maxTries) { |
| tries++; |
| long start = System.currentTimeMillis(); |
| String lun; |
| if (address.getConnectionId() == null) { |
| lun = "-"; |
| } else { |
| lun = address.getConnectionId(); |
| } |
| |
| Process p = null; |
| try { |
| ProcessBuilder builder = new ProcessBuilder(connectScript, lun, address.getAddress()); |
| p = builder.start(); |
| if (p.waitFor(scriptTimeoutSecs, TimeUnit.SECONDS)) { |
| int rc = p.exitValue(); |
| StringBuffer output = new StringBuffer(); |
| if (rc == 0) { |
| BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); |
| String line = null; |
| while ((line = input.readLine()) != null) { |
| output.append(line); |
| output.append(" "); |
| } |
| |
| physicalDisk = getPhysicalDisk(address, pool); |
| if (physicalDisk != null && physicalDisk.getSize() > 0) { |
| LOGGER.debug("Found the volume using id: " + address.getPath() + " of the storage pool: " + pool.getUuid()); |
| return true; |
| } |
| |
| break; |
| } else { |
| LOGGER.warn("Failure discovering LUN via " + connectScript); |
| BufferedReader error = new BufferedReader(new InputStreamReader(p.getErrorStream())); |
| String line = null; |
| while ((line = error.readLine()) != null) { |
| LOGGER.warn("error --> " + line); |
| } |
| } |
| } else { |
| LOGGER.debug("Timeout waiting for " + connectScript + " to complete - try " + tries); |
| } |
| } catch (IOException | InterruptedException | IllegalThreadStateException e) { |
| LOGGER.warn("Problem performing scan on SCSI hosts - try " + tries, e); |
| } finally { |
| if (p != null && p.isAlive()) { |
| p.destroyForcibly(); |
| } |
| } |
| |
| long elapsed = System.currentTimeMillis() - start; |
| waitTimeInMillis = waitTimeInMillis - elapsed; |
| |
| try { |
| Thread.sleep(timeBetweenTries); |
| } catch (Exception ex) { |
| // don't do anything |
| } |
| } |
| |
| LOGGER.debug("Unable to find the volume with id: " + address.getPath() + " of the storage pool: " + pool.getUuid()); |
| return false; |
| } |
| |
| void runConnectScript(String lun, AddressInfo address) { |
| try { |
| ProcessBuilder builder = new ProcessBuilder(connectScript, lun, address.getAddress()); |
| Process p = builder.start(); |
| int rc = p.waitFor(); |
| StringBuffer output = new StringBuffer(); |
| if (rc == 0) { |
| BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); |
| String line = null; |
| while ((line = input.readLine()) != null) { |
| output.append(line); |
| output.append(" "); |
| } |
| } else { |
| LOGGER.warn("Failure discovering LUN via " + connectScript); |
| BufferedReader error = new BufferedReader(new InputStreamReader(p.getErrorStream())); |
| String line = null; |
| while ((line = error.readLine()) != null) { |
| LOGGER.warn("error --> " + line); |
| } |
| } |
| } catch (IOException | InterruptedException e) { |
| throw new CloudRuntimeException("Problem performing scan on SCSI hosts", e); |
| } |
| } |
| |
| void sleep(long sleepTimeMs) { |
| try { |
| Thread.sleep(sleepTimeMs); |
| } catch (Exception ex) { |
| // don't do anything |
| } |
| } |
| |
| 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.debug("Unable to get the disk size at path: " + diskPath); |
| return 0; |
| } |
| |
| Long size = Long.parseLong(parser.getLine()); |
| |
| if (size <= 0) { |
| // its possible the path can't be seen on the host yet, lets rescan |
| // now rerun the command |
| parser = new OutputInterpreter.OneLineParser(); |
| result = diskCmd.execute(parser); |
| |
| if (result != null) { |
| LOGGER.debug("Unable to get the disk size at path: " + diskPath); |
| return 0; |
| } |
| |
| size = Long.parseLong(parser.getLine()); |
| } |
| |
| return size; |
| } |
| |
| public void resize(String path, String vmName, long newSize) { |
| if (LOGGER.isDebugEnabled()) LOGGER.debug("Executing resize of " + path + " to " + newSize + " bytes for VM " + vmName); |
| |
| // extract wwid |
| AddressInfo address = parseAndValidatePath(path); |
| if (address == null || address.getAddress() == null) { |
| LOGGER.error("Unable to resize volume, address value is not valid"); |
| throw new CloudRuntimeException("Unable to resize volume, address value is not valid"); |
| } |
| |
| if (LOGGER.isDebugEnabled()) LOGGER.debug(String.format("Running %s %s %s %s", resizeScript, address.getAddress(), vmName, newSize)); |
| |
| // call resizeVolume.sh <wwid> |
| ScriptResult result = runScript(resizeScript, 60000L, address.getAddress(), vmName, ""+newSize); |
| |
| if (result.getExitCode() != 0) { |
| throw new CloudRuntimeException("Failed to resize volume at address " + address.getAddress() + " to " + newSize + " bytes for VM " + vmName + ": " + result.getResult()); |
| } |
| |
| LOGGER.info("Resize of volume at address " + address.getAddress() + " completed successfully: " + result.getResult()); |
| } |
| |
| static void cleanupStaleMaps() { |
| synchronized(CLEANUP_LOCK) { |
| long start = System.currentTimeMillis(); |
| ScriptResult result = runScript(cleanupScript, cleanupTimeoutSecs * 1000); |
| LOGGER.debug("Multipath Cleanup Job elapsed time (ms): "+ (System.currentTimeMillis() - start) + "; result: " + result.getExitCode()); |
| } |
| } |
| |
| public static final class AddressInfo { |
| String type; |
| String address; |
| String connectionId; |
| String path; |
| |
| public AddressInfo(String type, String address, String connectionId, String path) { |
| this.type = type; |
| this.address = address; |
| this.connectionId = connectionId; |
| this.path = path; |
| } |
| |
| public String getType() { |
| return type; |
| } |
| |
| public String getAddress() { |
| return address; |
| } |
| |
| public String getConnectionId() { |
| return connectionId; |
| } |
| |
| public String getPath() { |
| return path; |
| } |
| |
| public String toString() { |
| return String.format("type=%s; address=%s; connid=%s", getType(), getAddress(), getConnectionId()); |
| } |
| } |
| |
| public static class Property <T> { |
| private String name; |
| private T defaultValue; |
| |
| Property(String name, T value) { |
| this.name = name; |
| this.defaultValue = value; |
| } |
| |
| public String getName() { |
| return this.name; |
| } |
| |
| public T getDefaultValue() { |
| return this.defaultValue; |
| } |
| |
| public T getFinalValue() { |
| File agentPropertiesFile = PropertiesUtil.findConfigFile("agent.properties"); |
| if (agentPropertiesFile == null) { |
| LOGGER.debug(String.format("File [%s] was not found, we will use default defined values. Property [%s]: [%s].", "agent.properties", name, defaultValue)); |
| return defaultValue; |
| } else { |
| try { |
| String configValue = PropertiesUtil.loadFromFile(agentPropertiesFile).getProperty(name); |
| if (StringUtils.isBlank(configValue)) { |
| LOGGER.debug(String.format("Property [%s] has empty or null value. Using default value [%s].", name, defaultValue)); |
| return defaultValue; |
| } else { |
| if (defaultValue instanceof Integer) { |
| return (T)Integer.getInteger(configValue); |
| } else if (defaultValue instanceof Long) { |
| return (T)Long.getLong(configValue); |
| } else if (defaultValue instanceof String) { |
| return (T)configValue; |
| } else if (defaultValue instanceof Boolean) { |
| return (T)Boolean.valueOf(configValue); |
| } else { |
| return null; |
| } |
| } |
| } catch (IOException var5) { |
| LOGGER.debug(String.format("Failed to get property [%s]. Using default value [%s].", name, defaultValue), var5); |
| return defaultValue; |
| } |
| } |
| } |
| } |
| |
| public static class ScriptResult { |
| private int exitCode = -1; |
| private String result = null; |
| public int getExitCode() { |
| return exitCode; |
| } |
| public void setExitCode(int exitCode) { |
| this.exitCode = exitCode; |
| } |
| public String getResult() { |
| return result; |
| } |
| public void setResult(String result) { |
| this.result = result; |
| } |
| } |
| |
| } |