// 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.util.HashMap; import java.util.List; import java.util.Map; import com.cloud.storage.Storage; import org.apache.log4j.Logger; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; import com.cloud.agent.api.to.DiskTO; import com.cloud.storage.Storage.ProvisioningType; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.utils.StringUtils; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; @StorageAdaptorInfo(storagePoolType=StoragePoolType.Iscsi) public class IscsiAdmStorageAdaptor implements StorageAdaptor { private static final Logger s_logger = Logger.getLogger(IscsiAdmStorageAdaptor.class); private static final Map<String, KVMStoragePool> MapStorageUuidToStoragePool = new HashMap<String, KVMStoragePool>(); @Override public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, StoragePoolType storagePoolType) { IscsiAdmStoragePool storagePool = new IscsiAdmStoragePool(uuid, host, port, storagePoolType, this); MapStorageUuidToStoragePool.put(uuid, storagePool); return storagePool; } @Override public KVMStoragePool getStoragePool(String uuid) { return MapStorageUuidToStoragePool.get(uuid); } @Override public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { return MapStorageUuidToStoragePool.get(uuid); } @Override public boolean deleteStoragePool(String uuid) { return MapStorageUuidToStoragePool.remove(uuid) != null; } @Override public boolean deleteStoragePool(KVMStoragePool pool) { return deleteStoragePool(pool.getUuid()); } // called from LibvirtComputingResource.execute(CreateCommand) // does not apply for iScsiAdmStorageAdaptor @Override public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, KVMStoragePool pool, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size) { throw new UnsupportedOperationException("Creating a physical disk is not supported."); } @Override public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<String, String> details) { // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 -o new Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, s_logger); iScsiAdmCmd.add("-m", "node"); iScsiAdmCmd.add("-T", getIqn(volumeUuid)); iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort()); iScsiAdmCmd.add("-o", "new"); String result = iScsiAdmCmd.execute(); if (result != null) { s_logger.debug("Failed to add iSCSI target " + volumeUuid); System.out.println("Failed to add iSCSI target " + volumeUuid); return false; } else { s_logger.debug("Successfully added iSCSI target " + volumeUuid); System.out.println("Successfully added to iSCSI target " + volumeUuid); } String chapInitiatorUsername = details.get(DiskTO.CHAP_INITIATOR_USERNAME); String chapInitiatorSecret = details.get(DiskTO.CHAP_INITIATOR_SECRET); if (StringUtils.isNotBlank(chapInitiatorUsername) && StringUtils.isNotBlank(chapInitiatorSecret)) { try { // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --op update -n node.session.auth.authmethod -v CHAP executeChapCommand(volumeUuid, pool, "node.session.auth.authmethod", "CHAP", null); // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --op update -n node.session.auth.username -v username executeChapCommand(volumeUuid, pool, "node.session.auth.username", chapInitiatorUsername, "username"); // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --op update -n node.session.auth.password -v password executeChapCommand(volumeUuid, pool, "node.session.auth.password", chapInitiatorSecret, "password"); } catch (Exception ex) { return false; } } // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10 --login iScsiAdmCmd = new Script(true, "iscsiadm", 0, s_logger); iScsiAdmCmd.add("-m", "node"); iScsiAdmCmd.add("-T", getIqn(volumeUuid)); iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort()); iScsiAdmCmd.add("--login"); result = iScsiAdmCmd.execute(); if (result != null) { s_logger.debug("Failed to log in to iSCSI target " + volumeUuid); System.out.println("Failed to log in to iSCSI target " + volumeUuid); return false; } else { s_logger.debug("Successfully logged in to iSCSI target " + volumeUuid); System.out.println("Successfully logged in to iSCSI target " + volumeUuid); } // There appears to be a race condition where logging in to the iSCSI volume via iscsiadm // returns success before the device has been added to the OS. // What happens is you get logged in and the device shows up, but the device may not // show up before we invoke Libvirt to attach the device to a VM. // waitForDiskToBecomeAvailable(String, KVMStoragePool) invokes blockdev // via getPhysicalDisk(String, KVMStoragePool) and checks if the size came back greater // than 0. // After a certain number of tries and a certain waiting period in between tries, // this method could still return (it should not block indefinitely) (the race condition // isn't solved here, but made highly unlikely to be a problem). waitForDiskToBecomeAvailable(volumeUuid, pool); return true; } private void waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) { int numberOfTries = 10; int timeBetweenTries = 1000; while (getPhysicalDisk(volumeUuid, pool).getSize() == 0 && numberOfTries > 0) { numberOfTries--; try { Thread.sleep(timeBetweenTries); } catch (Exception ex) { // don't do anything } } } private void executeChapCommand(String path, KVMStoragePool pool, String nParameter, String vParameter, String detail) throws Exception { Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, s_logger); iScsiAdmCmd.add("-m", "node"); iScsiAdmCmd.add("-T", getIqn(path)); iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort()); iScsiAdmCmd.add("--op", "update"); iScsiAdmCmd.add("-n", nParameter); iScsiAdmCmd.add("-v", vParameter); String result = iScsiAdmCmd.execute(); boolean useDetail = detail != null && detail.trim().length() > 0; detail = useDetail ? detail.trim() + " " : detail; if (result != null) { s_logger.debug("Failed to execute CHAP " + (useDetail ? detail : "") + "command for iSCSI target " + path + " : message = " + result); System.out.println("Failed to execute CHAP " + (useDetail ? detail : "") + "command for iSCSI target " + path + " : message = " + result); throw new Exception("Failed to execute CHAP " + (useDetail ? detail : "") + "command for iSCSI target " + path + " : message = " + result); } else { s_logger.debug("CHAP " + (useDetail ? detail : "") + "command executed successfully for iSCSI target " + path); System.out.println("CHAP " + (useDetail ? detail : "") + "command executed successfully for iSCSI target " + path); } } // example by-path: /dev/disk/by-path/ip-192.168.233.10:3260-iscsi-iqn.2012-03.com.solidfire:storagepool2-lun-0 private String getByPath(String host, String path) { return "/dev/disk/by-path/ip-" + host + "-iscsi-" + getIqn(path) + "-lun-" + getLun(path); } @Override public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { String deviceByPath = getByPath(pool.getSourceHost() + ":" + pool.getSourcePort(), volumeUuid); KVMPhysicalDisk physicalDisk = new KVMPhysicalDisk(deviceByPath, volumeUuid, pool); physicalDisk.setFormat(PhysicalDiskFormat.RAW); long deviceSize = getDeviceSize(deviceByPath); physicalDisk.setSize(deviceSize); physicalDisk.setVirtualSize(deviceSize); return physicalDisk; } private long getDeviceSize(String deviceByPath) { Script iScsiAdmCmd = new Script(true, "blockdev", 0, s_logger); iScsiAdmCmd.add("--getsize64", deviceByPath); OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); String result = iScsiAdmCmd.execute(parser); if (result != null) { s_logger.warn("Unable to retrieve the size of device " + deviceByPath); return 0; } return Long.parseLong(parser.getLine()); } private static String getIqn(String path) { return getComponent(path, 1); } private static String getLun(String path) { return getComponent(path, 2); } private static String getComponent(String path, int index) { String[] tmp = path.split("/"); if (tmp.length != 3) { String msg = "Wrong format for iScsi path: " + path + ". It should be formatted as '/targetIQN/LUN'."; s_logger.warn(msg); throw new CloudRuntimeException(msg); } return tmp[index].trim(); } public boolean disconnectPhysicalDisk(String host, int port, String iqn, String lun) { // use iscsiadm to log out of the iSCSI target and un-discover it // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10 --logout Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, s_logger); iScsiAdmCmd.add("-m", "node"); iScsiAdmCmd.add("-T", iqn); iScsiAdmCmd.add("-p", host + ":" + port); iScsiAdmCmd.add("--logout"); String result = iScsiAdmCmd.execute(); if (result != null) { s_logger.debug("Failed to log out of iSCSI target /" + iqn + "/" + lun + " : message = " + result); System.out.println("Failed to log out of iSCSI target /" + iqn + "/" + lun + " : message = " + result); return false; } else { s_logger.debug("Successfully logged out of iSCSI target /" + iqn + "/" + lun); System.out.println("Successfully logged out of iSCSI target /" + iqn + "/" + lun); } // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 -o delete iScsiAdmCmd = new Script(true, "iscsiadm", 0, s_logger); iScsiAdmCmd.add("-m", "node"); iScsiAdmCmd.add("-T", iqn); iScsiAdmCmd.add("-p", host + ":" + port); iScsiAdmCmd.add("-o", "delete"); result = iScsiAdmCmd.execute(); if (result != null) { s_logger.debug("Failed to remove iSCSI target /" + iqn + "/" + lun + " : message = " + result); System.out.println("Failed to remove iSCSI target /" + iqn + "/" + lun + " : message = " + result); return false; } else { s_logger.debug("Removed iSCSI target /" + iqn + "/" + lun); System.out.println("Removed iSCSI target /" + iqn + "/" + lun); } return true; } @Override public boolean disconnectPhysicalDisk(String volumeUuid, KVMStoragePool pool) { return disconnectPhysicalDisk(pool.getSourceHost(), pool.getSourcePort(), getIqn(volumeUuid), getLun(volumeUuid)); } @Override public boolean disconnectPhysicalDiskByPath(String localPath) { String search1 = "/dev/disk/by-path/ip-"; String search2 = ":"; String search3 = "-iscsi-"; String search4 = "-lun-"; if (localPath.indexOf(search3) == -1) { // this volume doesn't below to this adaptor, so just return true return true; } int index = localPath.indexOf(search2); String host = localPath.substring(search1.length(), index); int index2 = localPath.indexOf(search3); String port = localPath.substring(index + search2.length(), index2); index = localPath.indexOf(search4); String iqn = localPath.substring(index2 + search3.length(), index); String lun = localPath.substring(index + search4.length()); return disconnectPhysicalDisk(host, Integer.parseInt(port), iqn, lun); } @Override public boolean deletePhysicalDisk(String volumeUuid, KVMStoragePool pool, Storage.ImageFormat format) { throw new UnsupportedOperationException("Deleting a physical disk is not supported."); } // does not apply for iScsiAdmStorageAdaptor @Override public List<KVMPhysicalDisk> listPhysicalDisks(String storagePoolUuid, KVMStoragePool pool) { throw new UnsupportedOperationException("Listing disks is not supported for this configuration."); } @Override public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String name, PhysicalDiskFormat format, ProvisioningType provisioningType, long size, KVMStoragePool destPool, int timeout) { throw new UnsupportedOperationException("Creating a disk from a template is not yet supported for this configuration."); } @Override public KVMPhysicalDisk createTemplateFromDisk(KVMPhysicalDisk disk, String name, PhysicalDiskFormat format, long size, KVMStoragePool destPool) { throw new UnsupportedOperationException("Creating a template from a disk is not yet supported for this configuration."); } @Override public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout) { throw new UnsupportedOperationException("Copying a disk is not supported in this configuration."); } @Override public KVMPhysicalDisk createDiskFromSnapshot(KVMPhysicalDisk snapshot, String snapshotName, String name, KVMStoragePool destPool) { throw new UnsupportedOperationException("Creating a disk from a snapshot is not supported in this configuration."); } @Override public boolean refresh(KVMStoragePool pool) { return true; } @Override public boolean createFolder(String uuid, String path) { throw new UnsupportedOperationException("A folder cannot be created in this configuration."); } }