From 1b44cfa6044e518b1393585fee7bd15c452b8eca Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Mon, 20 Apr 2026 22:26:05 +0000 Subject: [PATCH] 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. tokens in the path still match in parseAndValidatePath on the KVM side. Followed by a matching adaptive/KVM driver change (separate commit). --- .../adapter/flasharray/FlashArrayAdapter.java | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java index 41125f3e113..f76d64bd2dc 100644 --- a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java +++ b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java @@ -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>() { }); - 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 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>() { }); + } else { + FlashArrayHost host = getHost(hostname); + if (host != null) { + list = POST("/connections?host_names=" + host.getName() + "&volume_names=" + volumeName, null, + new TypeReference>() { + }); + } } 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. 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()); } }