Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@
// under the License.
package com.cloud.hypervisor.kvm.storage;

import java.io.File;
import java.io.FileWriter;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Paths;

import org.apache.cloudstack.utils.qemu.QemuImg;
import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
Expand All @@ -47,12 +42,6 @@ public class IscsiAdmStorageAdaptor implements StorageAdaptor {

private static final Map<String, KVMStoragePool> MapStorageUuidToStoragePool = new HashMap<>();

/** iscsiadm's ISCSI_ERR_NO_OBJS_FOUND: returned by "-m session" when no session is established. */
private static final int ISCSI_ERR_NO_OBJS_FOUND = 21;

/** iscsiadm's ISCSI_ERR_SESS_EXISTS: returned by "--login" when the session is already logged in (e.g. Ubuntu). */
private static final int ISCSI_SESSION_EXISTS_CODE = 15;

@Override
public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, StoragePoolType storagePoolType, Map<String, String> details, boolean isPrimaryStorage) {
IscsiAdmStoragePool storagePool = new IscsiAdmStoragePool(uuid, host, port, storagePoolType, this);
Expand Down Expand Up @@ -96,22 +85,24 @@ public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, KVMStoragePool pool

@Override
public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<String, String> details, boolean isVMMigrate) {
final String host = pool.getSourceHost();
final int port = pool.getSourcePort();
final String iqn = getIqn(volumeUuid);

// 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, logger);

iScsiAdmCmd.add("-m", "node");
iScsiAdmCmd.add("-T", iqn);
iScsiAdmCmd.add("-p", host + ":" + port);
iScsiAdmCmd.add("-T", getIqn(volumeUuid));
iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort());
iScsiAdmCmd.add("-o", "new");

String result = iScsiAdmCmd.execute();

if (!handleNodeCreateResult(result, volumeUuid)) {
if (result != null) {
logger.debug("Failed to add iSCSI target " + volumeUuid);
System.out.println("Failed to add iSCSI target " + volumeUuid);

return false;
} else {
logger.debug("Successfully added iSCSI target " + volumeUuid);
System.out.println("Successfully added to iSCSI target " + volumeUuid);
}

String chapInitiatorUsername = details.get(DiskTO.CHAP_INITIATOR_USERNAME);
Expand All @@ -132,10 +123,24 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
}
}

// Login is always attempted (idempotent). Rescan runs only if the session already existed
// before login (Oracle re-login exits 0; Ubuntu may return ISCSI_ERR_SESS_EXISTS).
if (!loginOrRescanExistingSession(iqn, host, port, volumeUuid)) {
// ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --login
iScsiAdmCmd = new Script(true, "iscsiadm", 0, 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) {
logger.debug("Failed to log in to iSCSI target " + volumeUuid);
System.out.println("Failed to log in to iSCSI target " + volumeUuid);

return false;
} else {
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
Expand All @@ -148,137 +153,24 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
// 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).
// If the by-path is missing or is a regular file (not the iSCSI block symlink), size
// stays 0. Return false so connect does not succeed and a raw file is not created at
// that by-path in place of the real LUN device.
if (!waitForDiskToBecomeAvailable(volumeUuid, pool)) {
logger.warn("iSCSI device not ready for target {} at {}:{} after wait", volumeUuid, host, port);
return false;
}
waitForDiskToBecomeAvailable(volumeUuid, pool);

return true;
}

/**
* Checks the result of an iscsiadm node-create command.
* Returns true if the node was created or already exists, false on failure.
*/
boolean handleNodeCreateResult(String result, String volumeUuid) {
if (result == null) {
logger.debug("Successfully added iSCSI node for target {}", volumeUuid);
return true;
}
String msg = result.toLowerCase();
if (msg.contains("already exists") || msg.contains("database exists") || msg.contains("exists")) {
logger.debug("iSCSI node already exists for target {}, proceeding", volumeUuid);
return true;
}
logger.debug("Failed to add iSCSI node for target {}: {}", volumeUuid, result);
return false;
}

/**
* Checks existing session state, performs login, and rescans only if the session already existed.
*
* Login is always attempted (idempotent). A pre-login session check is required on Oracle,
* where re-login often exits 0; Ubuntu may instead return ISCSI_ERR_SESS_EXISTS (15).
* Session-preexisted must be treated as success first: on Ubuntu, re-login exits 15 with a
* non-null error message that would otherwise be treated as failure.
*
* @return true if login succeeded (and rescan ran when needed), false on login failure
*/
private boolean loginOrRescanExistingSession(String iqn, String host, int port, String volumeUuid) {
boolean sessionAlreadyActive = isIscsiSessionActive(iqn, host, port);
logger.debug("iSCSI session active check for target {} at {}:{}: {}", iqn, host, port, sessionAlreadyActive);

Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
iScsiAdmCmd.add("-m", "node");
iScsiAdmCmd.add("-T", iqn);
iScsiAdmCmd.add("-p", host + ":" + port);
iScsiAdmCmd.add("--login");

String result = iScsiAdmCmd.execute();
boolean sessionPreExisted = (iScsiAdmCmd.getExitValue() == ISCSI_SESSION_EXISTS_CODE) || sessionAlreadyActive;

if (sessionPreExisted) {
logger.debug("iSCSI session for target {} at {}:{} pre-existed, performing rescan", iqn, host, port);
rescanIscsiSessions(iqn, host, port);
return true;
}
if (result == null) {
logger.debug("Successfully logged in to iSCSI target {}", volumeUuid);
return true;
}
logger.debug("Failed to log in to iSCSI target {}: {}", volumeUuid, result);
return false;
}

/**
* Checks whether a session to the given target and portal is already established.
*
* "iscsiadm -m session" exits with ISCSI_ERR_NO_OBJS_FOUND when no session exists, which is a
* normal outcome here. Any other non-zero exit is logged and treated as not confirmed active.
*/
private boolean isIscsiSessionActive(String iqn, String host, int port) {
Script sessionCmd = new Script(true, "iscsiadm", 0, logger);
sessionCmd.add("-m", "session");

OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
sessionCmd.executeIgnoreExitValue(parser, ISCSI_ERR_NO_OBJS_FOUND);
int exitValue = sessionCmd.getExitValue();
if (exitValue != 0 && exitValue != ISCSI_ERR_NO_OBJS_FOUND) {
logger.warn("Unable to determine iSCSI session state for target {} at {}:{}: 'iscsiadm -m session' exited with {}",
iqn, host, port, exitValue);
return false;
}

String sessions = parser.getLines();
if (StringUtils.isBlank(sessions)) {
return false;
}
// AllLinesParser uses BufferedReader.readLine() (strips \n, \r\n, and \r) and then
// appends "\n" after each session. split("\n") depends on that separator to walk
// one session per line when multiple sessions are listed.
for (String line : sessions.split("\n")) {
if (line.contains(iqn) && line.contains(host)) {
return true;
}
}

return false;
}

private void rescanIscsiSessions(String iqn, String host, int port) {
Script rescanCmd = new Script(true, "iscsiadm", 0, logger);
rescanCmd.add("-m", "node");
rescanCmd.add("-T", iqn);
rescanCmd.add("-p", host + ":" + port);
rescanCmd.add("--rescan");
String rescanResult = rescanCmd.execute();
if (rescanResult != null) {
logger.warn("iSCSI session rescan returned: {}", rescanResult);
} else {
logger.debug("iSCSI session rescan completed successfully for {}@{}:{}", iqn, host, port);
}
}

private boolean waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) {
private void waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) {
int numberOfTries = 10;
int timeBetweenTries = 1000;
long deviceSize = 0;

while ((deviceSize = getPhysicalDisk(volumeUuid, pool).getSize()) == 0 && numberOfTries > 0) {
while (getPhysicalDisk(volumeUuid, pool).getSize() == 0 && numberOfTries > 0) {
numberOfTries--;

try {
Thread.sleep(timeBetweenTries);
} catch (InterruptedException ex) {
logger.warn("Interrupted while waiting for iSCSI device {} to become available", volumeUuid, ex);
return false;
} catch (Exception ex) {
// don't do anything
}
}

return deviceSize > 0;
}

private void waitForDiskToBecomeUnavailable(String host, int port, String iqn, String lun) {
Expand Down Expand Up @@ -346,25 +238,6 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) {
}

private long getDeviceSize(String deviceByPath) {
try {
Path devicePath = Paths.get(deviceByPath);
if (!Files.exists(devicePath)) {
logger.debug("Device by-path does not exist yet: {}", deviceByPath);
return 0L;
}
if (Files.isRegularFile(devicePath)) {
logger.warn("Found a corrupt regular file at iSCSI by-path {} (expected block device symlink); it must be removed manually", deviceByPath);
return 0L;
}
if (!Files.isSymbolicLink(devicePath)) {
logger.warn("Path {} exists but is not an iSCSI block device symlink", deviceByPath);
return 0L;
}
} catch (Exception ex) {
// If FS check fails for any reason, fall back to blockdev call
logger.error("Error fetching device size for {}", deviceByPath, ex);
}

Script iScsiAdmCmd = new Script(true, "blockdev", 0, logger);

iScsiAdmCmd.add("--getsize64", deviceByPath);
Expand Down Expand Up @@ -407,96 +280,8 @@ private String getComponent(String path, int index) {
return tmp[index].trim();
}

/**
* Check if there are other LUNs on the same iSCSI target (IQN) that are still
* visible as block devices. This is needed because ONTAP uses a single IQN per
* SVM — logging out of the target would kill ALL LUNs, not just the one being
* disconnected.
*
* Checks /dev/disk/by-path/ for symlinks matching the same host:port + IQN but
* with a different LUN number.
*/
private boolean hasOtherActiveLuns(String host, int port, String iqn, String lun) {
String prefix = "ip-" + host + ":" + port + "-iscsi-" + iqn + "-lun-";
File byPathDir = new File("/dev/disk/by-path");
if (!byPathDir.exists() || !byPathDir.isDirectory()) {
return false;
}
File[] entries = byPathDir.listFiles();
if (entries == null) {
return false;
}
for (File entry : entries) {
String name = entry.getName();
// Skip partition entries (e.g. lun-0-part1, lun-0-part2) — these are not
// independent LUNs, they are partition symlinks for the same LUN disk.
// Only count actual LUN entries (no "-part" suffix after the lun number).
if (name.startsWith(prefix) && !name.equals(prefix + lun) && !name.contains("-part")) {
logger.debug("Found other active LUN on same target: " + name);
return true;
}
}
return false;
}

/**
* Removes a single stale SCSI device from the kernel using the sysfs interface.
*
* When ONTAP unmaps a LUN from the host's igroup, the by-path symlink and the
* underlying SCSI device (/dev/sdX) remain present in the kernel until explicitly
* removed — the kernel does not auto-remove devices from live iSCSI sessions.
*
* This method resolves the by-path symlink to the real block device name (e.g. sdd),
* then writes "1" to /sys/block/<dev>/device/delete — the standard Linux kernel SCSI
* API for removing a single device without tearing down the entire iSCSI session.
* Once the kernel processes the delete, it also removes the by-path symlink.
*
* This is used instead of iscsiadm --logout when other LUNs on the same IQN are still
* active (ONTAP single-IQN-per-SVM model), since logout would tear down ALL LUNs.
*/
private void removeStaleScsiDevice(String host, int port, String iqn, String lun) {
String byPath = getByPath(host, port, "/" + iqn + "/" + lun);
Path byPathLink = Paths.get(byPath);
if (!Files.exists(byPathLink)) {
logger.debug("by-path entry for LUN " + lun + " already gone, nothing to remove");
return;
}
try {
Path realDevice = byPathLink.toRealPath();
String devName = realDevice.getFileName().toString();
File deleteFile = new File("/sys/block/" + devName + "/device/delete");
if (!deleteFile.exists()) {
logger.warn("sysfs delete entry not found for device " + devName + " — cannot remove stale SCSI device");
return;
}
try (FileWriter fw = new FileWriter(deleteFile)) {
fw.write("1");
}
logger.info("Removed stale SCSI device " + devName + " for LUN /" + iqn + "/" + lun + " via sysfs");
} catch (Exception e) {
logger.warn("Failed to remove stale SCSI device for LUN /" + iqn + "/" + lun + ": " + e.getMessage());
}
}

private boolean disconnectPhysicalDisk(String host, int port, String iqn, String lun) {
// Check if other LUNs on the same IQN target are still in use.
// ONTAP (and similar) uses a single IQN per SVM with multiple LUNs.
// Doing iscsiadm --logout tears down the ENTIRE target session,
// which would destroy access to ALL LUNs — not just the one being disconnected.
if (hasOtherActiveLuns(host, port, iqn, lun)) {
logger.info("Skipping iSCSI logout for /" + iqn + "/" + lun +
" — other LUNs on the same target are still active. Removing stale SCSI device for this LUN only.");
removeStaleScsiDevice(host, port, iqn, lun);
// After removing this LUN's device, re-check: if no other LUNs remain active,
// If it is the last one then must logout to clean up the iSCSI session entirely.
if (hasOtherActiveLuns(host, port, iqn, lun)) {
logger.info("Other LUNs still active after removing /" + iqn + "/" + lun + " — session kept alive.");
return true;
}
logger.info("No more active LUNs on target after removing /" + iqn + "/" + lun + " — proceeding with iSCSI logout.");
}

// No other LUNs active on this target — safe to logout and delete the node record.
// 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:3260 --logout
Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
Expand Down Expand Up @@ -602,8 +387,8 @@ public List<KVMPhysicalDisk> listPhysicalDisks(String storagePoolUuid, KVMStorag

@Override
public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String name, PhysicalDiskFormat format,
ProvisioningType provisioningType, long size,
KVMStoragePool destPool, int timeout, byte[] passphrase) {
ProvisioningType provisioningType, long size,
KVMStoragePool destPool, int timeout, byte[] passphrase) {
throw new UnsupportedOperationException("Creating a disk from a template is not yet supported for this configuration.");
}

Expand Down Expand Up @@ -637,19 +422,6 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk srcDisk, String destVolu
try {
QemuImg q = new QemuImg(timeout);
q.convert(srcFile, destFile);
// Below fix is required when vendor depends on host based copy rather than storage CAN_CREATE_VOLUME_FROM_VOLUME capability
// When host based template copy is triggered , small size template sits in RAM(depending on host memory and RAM) and copy is marked successful and by the time flush to storage is triggered
// disconnectPhysicalDisk would disconnect the lun , hence template staying in RAM is not copied to storage lun. Below does flushing of data to storage and marking
// copy as successful once flush is complete.
Script flushCmd = new Script(true, "blockdev", 0, logger);
flushCmd.add("--flushbufs", destDisk.getPath());
String flushResult = flushCmd.execute();
if (flushResult != null) {
logger.warn("iSCSI copyPhysicalDisk: blockdev --flushbufs returned: {}", flushResult);
}
Script syncCmd = new Script(true, "sync", 0, logger);
syncCmd.execute();
logger.info("iSCSI copyPhysicalDisk: flush/sync completed ");
} catch (QemuImgException | LibvirtException ex) {
String msg = "Failed to copy data from " + srcDisk.getPath() + " to " +
destDisk.getPath() + ". The error was the following: " + ex.getMessage();
Expand Down
Loading
Loading