flasharray: support NVMe-TCP transport

Teach FlashArrayAdapter to talk to a pool over NVMe over TCP instead of
Fibre Channel.

The transport is selected from a new transport= option on the storage
pool URL (or the equivalent storage_pool_details entry), e.g.

    https://user:pass@fa:443/api?pod=cs&transport=nvme-tcp&hostgroup=cluster1

Defaults remain Fibre Channel / WWN addressing when transport is absent
or anything other than nvme-tcp, so existing FC pools are unaffected.

Beyond the transport parsing itself the adapter now:

  * Tracks a per-pool volumeAddressType (AddressType.NVMETCP or
    FIBERWWN) and stamps every volume it hands back to the framework
    with it (withAddressType), so the adaptive driver path stores the
    correct type=... field in the CloudStack volume path (used later
    by the KVM driver to locate the device).

  * Attaches pod-backed NVMe-TCP volumes at the host-group level
    (POST /connections?host_group_names=...) instead of per-host, so
    the array assigns a consistent NSID to every member host; falls
    back to per-host attach for FC or when no hostgroup is configured.

  * Tolerates a missing nsid in the FlashArray connections response
    for NVMe-TCP - Purity does not return one for host-group NVMe
    connections; the namespace is identified on the host by EUI-128
    from FlashArrayVolume.getAddress(), so a placeholder value is
    returned to the caller purely for informational tracking.

  * Resolves NVMETCP addresses back to volumes in getVolumeByAddress
    by reversing the EUI-128 layout (strip optional eui. prefix, drop
    leading 00 and the embedded Pure OUI).

  * Indexes NVMe connections in getConnectionIdMap by host name (the
    array returns one entry per host inside a host-group connection),
    so connid.<hostname> tokens in the path still match in
    parseAndValidatePath on the KVM side.

Followed by a matching adaptive/KVM driver change (separate commit).
This commit is contained in:
Eugenio Grosso 2026-04-20 22:26:05 +00:00
parent c3c0f0cedd
commit 1b44cfa604
1 changed files with 65 additions and 15 deletions

View File

@ -73,6 +73,9 @@ public class FlashArrayAdapter implements ProviderAdapter {
public static final String HOSTGROUP = "hostgroup";
public static final String STORAGE_POD = "pod";
public static final String TRANSPORT = "transport";
public static final String TRANSPORT_FC = "fc";
public static final String TRANSPORT_NVME_TCP = "nvme-tcp";
public static final String KEY_TTL = "keyttl";
public static final String CONNECT_TIMEOUT_MS = "connectTimeoutMs";
public static final String POST_COPY_WAIT_MS = "postCopyWaitMs";
@ -88,6 +91,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
static final ObjectMapper mapper = new ObjectMapper();
public String pod = null;
public String hostgroup = null;
private AddressType volumeAddressType = AddressType.FIBERWWN;
private String username;
private String password;
private String accessToken;
@ -121,7 +125,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
request, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
return (ProviderVolume) getFlashArrayItem(list);
return withAddressType((FlashArrayVolume) getFlashArrayItem(list));
}
/**
@ -140,11 +144,19 @@ public class FlashArrayAdapter implements ProviderAdapter {
String volumeName = normalizeName(pod, dataObject.getExternalName());
try {
FlashArrayList<FlashArrayConnection> list = null;
FlashArrayHost host = getHost(hostname);
if (host != null) {
list = POST("/connections?host_names=" + host.getName() + "&volume_names=" + volumeName, null,
if (AddressType.NVMETCP.equals(volumeAddressType) && hostgroup != null) {
// NVMe-TCP pod volumes are connected at the host-group level so the
// array assigns a consistent NSID visible to every member host.
list = POST("/connections?host_group_names=" + hostgroup + "&volume_names=" + volumeName, null,
new TypeReference<FlashArrayList<FlashArrayConnection>>() {
});
} else {
FlashArrayHost host = getHost(hostname);
if (host != null) {
list = POST("/connections?host_names=" + host.getName() + "&volume_names=" + volumeName, null,
new TypeReference<FlashArrayList<FlashArrayConnection>>() {
});
}
}
if (list == null || list.getItems() == null || list.getItems().size() == 0) {
@ -152,10 +164,16 @@ public class FlashArrayAdapter implements ProviderAdapter {
}
FlashArrayConnection connection = (FlashArrayConnection) this.getFlashArrayItem(list);
if (AddressType.NVMETCP.equals(volumeAddressType)) {
// The FlashArray REST API does not return nsid in the connections
// payload for NVMe-TCP. The namespace is identified on the host by
// EUI-128 (see FlashArrayVolume.getAddress()); the value returned
// here is stored by the driver only for informational purposes.
return connection.getNsid() != null ? "" + connection.getNsid() : "1";
}
if (connection.getLun() == null) {
throw new RuntimeException("Volume attach missing lun field");
}
return "" + connection.getLun();
} catch (Throwable e) {
@ -167,13 +185,18 @@ public class FlashArrayAdapter implements ProviderAdapter {
});
if (list != null && list.getItems() != null) {
for (FlashArrayConnection conn : list.getItems()) {
if (conn.getHost() != null && conn.getHost().getName() != null &&
if (AddressType.NVMETCP.equals(volumeAddressType)) {
if (conn.getHostGroup() != null && conn.getHostGroup().getName() != null
&& conn.getHostGroup().getName().equals(hostgroup)) {
return conn.getNsid() != null ? "" + conn.getNsid() : "1";
}
} else if (conn.getHost() != null && conn.getHost().getName() != null &&
(conn.getHost().getName().equals(hostname) || conn.getHost().getName().equals(hostname.substring(0, hostname.indexOf('.')))) &&
conn.getLun() != null) {
return "" + conn.getLun();
}
}
throw new RuntimeException("Volume lun is not found in existing connection");
throw new RuntimeException("Volume connection identifier (lun/nsid) not found in existing connection");
} else {
throw new RuntimeException("Volume lun is not found in existing connection");
}
@ -238,7 +261,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
}
FlashArrayVolume volume = null;
try {
volume = getVolume(externalName);
volume = withAddressType(getVolume(externalName));
// if we didn't get an address back its likely an empty object
if (volume != null && volume.getAddress() == null) {
return null;
@ -260,14 +283,19 @@ public class FlashArrayAdapter implements ProviderAdapter {
throw new RuntimeException("Invalid search criteria provided for getVolumeByAddress");
}
// only support WWN type addresses at this time.
if (!ProviderVolume.AddressType.FIBERWWN.equals(addressType)) {
String serial;
if (ProviderVolume.AddressType.FIBERWWN.equals(addressType)) {
// Strip the NAA prefix (1 char) + Pure OUI to recover the volume serial.
serial = address.substring(FlashArrayVolume.PURE_OUI.length() + 1).toUpperCase();
} else if (ProviderVolume.AddressType.NVMETCP.equals(addressType)) {
// Reverse the EUI-128 layout: serial = eui[2:16] + eui[22:32], after
// stripping the optional "eui." prefix that appears in udev paths.
String eui = address.startsWith("eui.") ? address.substring(4) : address;
serial = (eui.substring(2, 16) + eui.substring(22)).toUpperCase();
} else {
throw new RuntimeException(
"Invalid volume address type [" + addressType + "] requested for volume search");
}
// convert WWN to serial to search on. strip out WWN type # + Flash OUI value
String serial = address.substring(FlashArrayVolume.PURE_OUI.length() + 1).toUpperCase();
String query = "serial='" + serial + "'";
FlashArrayVolume volume = null;
@ -281,7 +309,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
return null;
}
volume = (FlashArrayVolume) this.getFlashArrayItem(list);
volume = withAddressType((FlashArrayVolume) this.getFlashArrayItem(list));
if (volume != null && volume.getAddress() == null) {
return null;
}
@ -599,6 +627,13 @@ public class FlashArrayAdapter implements ProviderAdapter {
}
}
String transport = connectionDetails.get(FlashArrayAdapter.TRANSPORT);
if (transport == null) {
transport = queryParms.get(FlashArrayAdapter.TRANSPORT);
}
volumeAddressType = TRANSPORT_NVME_TCP.equalsIgnoreCase(transport)
? AddressType.NVMETCP : AddressType.FIBERWWN;
// retrieve for legacy purposes. if set, we'll remove any connections to hostgroup we find and use the host
hostgroup = connectionDetails.get(FlashArrayAdapter.HOSTGROUP);
if (hostgroup == null) {
@ -781,6 +816,13 @@ public class FlashArrayAdapter implements ProviderAdapter {
return (FlashArrayVolume) getFlashArrayItem(list);
}
private FlashArrayVolume withAddressType(FlashArrayVolume vol) {
if (vol != null) {
vol.setAddressType(volumeAddressType);
}
return vol;
}
private Object getFlashArrayItem(FlashArrayList<?> list) {
if (list.getItems() != null && list.getItems().size() > 0) {
return list.getItems().get(0);
@ -1087,7 +1129,15 @@ public class FlashArrayAdapter implements ProviderAdapter {
if (list != null && list.getItems() != null) {
for (FlashArrayConnection conn : list.getItems()) {
if (conn.getHost() != null) {
if (AddressType.NVMETCP.equals(volumeAddressType)) {
// Host-group-scoped NVMe connections come back as one
// entry per host in the group; key on the host name so
// connid.<hostname> is matched in parseAndValidatePath.
if (conn.getHost() != null && conn.getHost().getName() != null) {
String id = conn.getNsid() != null ? "" + conn.getNsid() : "1";
map.put(conn.getHost().getName(), id);
}
} else if (conn.getHost() != null) {
map.put(conn.getHost().getName(), "" + conn.getLun());
}
}