Skip to content
Closed
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
5 changes: 2 additions & 3 deletions plugins/storage/volume/ontap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.22.0.0-SNAPSHOT</version>
<version>4.23.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<properties>
<spring-cloud.version>2021.0.7</spring-cloud.version>
<openfeign.version>11.0</openfeign.version>
<json.version>20230227</json.version>
<jackson-databind.version>2.15.2</jackson-databind.version>
<httpclient.version>4.5.14</httpclient.version>
<swagger-annotations.version>1.6.2</swagger-annotations.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
Expand Down Expand Up @@ -77,7 +76,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-databind.version}</version>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePool;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
Expand Down Expand Up @@ -64,13 +65,14 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {

@Inject private StoragePoolDetailsDao storagePoolDetailsDao;
@Inject private PrimaryDataStoreDao storagePoolDao;
@Inject private com.cloud.storage.dao.VolumeDao volumeDao;
@Override
public Map<String, String> getCapabilities() {
s_logger.trace("OntapPrimaryDatastoreDriver: getCapabilities: Called");
Map<String, String> mapCapabilities = new HashMap<>();

mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
// RAW managed initial implementation: snapshot features not yet supported
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.FALSE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.FALSE.toString());

return mapCapabilities;
}
Expand Down Expand Up @@ -116,33 +118,156 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
createCmdResult = new CreateCmdResult(null, new Answer(null, false, errMsg));
createCmdResult.setResult(e.toString());
} finally {
if (createCmdResult != null && createCmdResult.isSuccess()) {
s_logger.info("createAsync: Volume metadata created successfully. Path: {}", path);
}
callback.complete(createCmdResult);
}
}

/**
* Creates CloudStack volume based on storage protocol type (NFS or iSCSI).
*
* For Managed NFS (Option 2 Implementation):
* - Returns only UUID without creating qcow2 file
* - KVM hypervisor creates qcow2 file automatically during VM deployment
* - ONTAP volume provides the backing NFS storage
*
* For iSCSI/Block Storage:
* - Creates LUN via ONTAP REST API
* - Returns LUN path for direct attachment
*/
private String createCloudStackVolumeForTypeVolume(DataStore dataStore, DataObject dataObject) {
StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId());
if(storagePool == null) {
s_logger.error("createCloudStackVolume : Storage Pool not found for id: " + dataStore.getId());
throw new CloudRuntimeException("createCloudStackVolume : Storage Pool not found for id: " + dataStore.getId());
s_logger.error("createCloudStackVolumeForTypeVolume: Storage Pool not found for id: {}", dataStore.getId());
throw new CloudRuntimeException("createCloudStackVolumeForTypeVolume: Storage Pool not found for id: " + dataStore.getId());
}

Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(dataStore.getId());
String protocol = details.get(Constants.PROTOCOL);

if (ProtocolType.NFS.name().equalsIgnoreCase(protocol)) {
return createManagedNfsVolume(dataStore, dataObject, storagePool);
} else if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) {
return createManagedBlockVolume(dataStore, dataObject, storagePool, details);
} else {
String errMsg = String.format("createCloudStackVolumeForTypeVolume: Unsupported protocol [%s]", protocol);
s_logger.error(errMsg);
throw new CloudRuntimeException(errMsg);
}
}

/**
* Creates Managed NFS Volume with ONTAP backing storage.
*
* Architecture: 1 CloudStack Storage Pool = 1 ONTAP Volume (shared by all volumes)
*
* Flow:
* 1. createAsync() stores volume metadata and NFS mount point
* 2. Volume attach triggers ManagedNfsStorageAdaptor.connectPhysicalDisk()
* 3. KVM mounts: nfs://nfsServer/junctionPath to /mnt/volumeUuid
* 4. Libvirt creates qcow2 file via storageVolCreateXML()
* 5. File created at: /vol/ontap_volume/volumeUuid (on ONTAP)
*
* Key Details:
* - All volumes in same pool share the same ONTAP volume NFS export
* - Each volume gets separate libvirt mount point: /mnt/<volumeUuid>
* - All qcow2 files stored in same ONTAP volume: /vol/<pool_volume_name>/
* - volume._iScsiName stores the NFS junction path (pool.path)
*
* @param dataStore CloudStack data store (storage pool)
* @param dataObject Volume data object
* @param storagePool Storage pool VO
* @return Volume UUID (used as filename for qcow2 file)
*/
private String createManagedNfsVolume(DataStore dataStore, DataObject dataObject, StoragePoolVO storagePool) {
VolumeInfo volumeInfo = (VolumeInfo) dataObject;
VolumeVO volume = volumeDao.findById(volumeInfo.getId());
String volumeUuid = volumeInfo.getUuid();

// Get the NFS junction path from storage pool
// This is the path that was set during pool creation (e.g., "/my_pool_volume")
String junctionPath = storagePool.getPath();

// Update volume metadata in CloudStack database
volume.setPoolType(Storage.StoragePoolType.ManagedNFS);
volume.setPoolId(dataStore.getId());
volume.setPath(volumeUuid); // Filename for qcow2 file

// CRITICAL: Store junction path in _iScsiName field
// CloudStack will use this in AttachCommand as DiskTO.MOUNT_POINT
// ManagedNfsStorageAdaptor will mount: nfs://hostAddress/junctionPath to /mnt/volumeUuid
volume.set_iScsiName(junctionPath);

volumeDao.update(volume.getId(), volume);

s_logger.info("ONTAP Managed NFS Volume Created: uuid={}, path={}, junctionPath={}, format=QCOW2, " +
"pool={}, size={}GB. Libvirt will create qcow2 file at mount time.",
volumeUuid, volumeUuid, junctionPath, storagePool.getName(),
volumeInfo.getSize() / (1024 * 1024 * 1024));

// Optional: Prepare ONTAP volume for optimal qcow2 storage (future enhancement)
// prepareOntapVolumeForQcow2Storage(dataStore, volumeInfo);

return volumeUuid;
}

/**
* Creates iSCSI/Block volume by calling ONTAP REST API to create a LUN.
*
* For block storage (iSCSI), the storage provider must create the LUN
* before CloudStack can use it. This is different from NFS where the
* hypervisor creates the file.
*
* @param dataStore CloudStack data store
* @param dataObject Volume data object
* @param storagePool Storage pool VO
* @param details Storage pool details containing ONTAP connection info
* @return LUN path/name for iSCSI attachment
*/
private String createManagedBlockVolume(DataStore dataStore, DataObject dataObject,
StoragePoolVO storagePool, Map<String, String> details) {
StorageStrategy storageStrategy = getStrategyByStoragePoolDetails(details);
s_logger.info("createCloudStackVolumeForTypeVolume: Connection to Ontap SVM [{}] successful, preparing CloudStackVolumeRequest", details.get(Constants.SVM_NAME));
CloudStackVolume cloudStackVolumeRequest = Utility.createCloudStackVolumeRequestByProtocol(storagePool, details, dataObject);

s_logger.info("createManagedBlockVolume: Creating iSCSI LUN on ONTAP SVM [{}]", details.get(Constants.SVM_NAME));

CloudStackVolume cloudStackVolumeRequest = Utility.createCloudStackVolumeRequestByProtocol(storagePool, details, (VolumeInfo) dataObject);

CloudStackVolume cloudStackVolume = storageStrategy.createCloudStackVolume(cloudStackVolumeRequest);
if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL)) && cloudStackVolume.getLun() != null && cloudStackVolume.getLun().getName() != null) {
return cloudStackVolume.getLun().getName();

if (cloudStackVolume.getLun() != null && cloudStackVolume.getLun().getName() != null) {
String lunPath = cloudStackVolume.getLun().getName();
s_logger.info("createManagedBlockVolume: iSCSI LUN created successfully: {}", lunPath);
return lunPath;
} else {
String errMsg = "createCloudStackVolumeForTypeVolume: Volume creation failed. Lun or Lun Path is null for dataObject: " + dataObject;
String errMsg = String.format("createManagedBlockVolume: LUN creation failed for volume [%s]. " +
"LUN or LUN path is null.", dataObject.getUuid());
s_logger.error(errMsg);
throw new CloudRuntimeException(errMsg);
}
}

@Override
public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback<CommandResult> callback) {

CommandResult commandResult = new CommandResult();
try {
if (store == null || data == null) {
throw new CloudRuntimeException("deleteAsync: store or data is null");
}
if (data.getType() == DataObjectType.VOLUME) {
StoragePoolVO storagePool = storagePoolDao.findById(store.getId());
Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(store.getId());
if (ProtocolType.NFS.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) {
// ManagedNFS qcow2 backing file deletion handled by KVM host/libvirt; nothing to do via ONTAP REST.
s_logger.info("deleteAsync: ManagedNFS volume {} no-op ONTAP deletion", data.getId());
}
}
} catch (Exception e) {
commandResult.setResult(e.getMessage());
} finally {
callback.complete(commandResult);
}
}

@Override
Expand Down Expand Up @@ -177,7 +302,6 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore

@Override
public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) {

}

@Override
Expand Down Expand Up @@ -217,7 +341,7 @@ public void handleQualityOfServiceForVolumeMigration(VolumeInfo volumeInfo, Qual

@Override
public boolean canProvideStorageStats() {
return true;
return false;
}

@Override
Expand All @@ -227,7 +351,7 @@ public Pair<Long, Long> getStorageStats(StoragePool storagePool) {

@Override
public boolean canProvideVolumeStats() {
return true;
return false; // Not yet implemented for RAW managed NFS
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* 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 org.apache.cloudstack.storage.feign;

import feign.RequestInterceptor;
Expand All @@ -11,7 +30,7 @@
import feign.codec.EncodeException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
Expand All @@ -36,13 +55,11 @@ public class FeignConfiguration {
private final int retryMaxInterval = 5;
private final String ontapFeignMaxConnection = "80";
private final String ontapFeignMaxConnectionPerRoute = "20";
private final JsonMapper jsonMapper;
private final ObjectMapper jsonMapper;

public FeignConfiguration() {
this.jsonMapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.findAndAddModules()
.build();
this.jsonMapper = new ObjectMapper();
this.jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

public Client createClient() {
Expand Down Expand Up @@ -120,16 +137,43 @@ public Decoder createDecoder() {
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException {
if (response.body() == null) {
logger.debug("Response body is null, returning null");
return null;
}
String json = null;
try (InputStream bodyStream = response.body().asInputStream()) {
json = new String(bodyStream.readAllBytes(), StandardCharsets.UTF_8);
logger.debug("Decoding JSON response: {}", json);
return jsonMapper.readValue(json, jsonMapper.getTypeFactory().constructType(type));
logger.debug("Target type: {}", type);
logger.debug("About to call jsonMapper.readValue()...");

Object result = null;
try {
logger.debug("Calling jsonMapper.constructType()...");
var javaType = jsonMapper.getTypeFactory().constructType(type);
logger.debug("constructType() returned: {}", javaType);

logger.debug("Calling jsonMapper.readValue() with json and javaType...");
result = jsonMapper.readValue(json, javaType);
logger.debug("jsonMapper.readValue() completed successfully");
} catch (Throwable ex) {
logger.error("EXCEPTION in jsonMapper.readValue()! Type: {}, Message: {}", ex.getClass().getName(), ex.getMessage(), ex);
throw ex;
}

if (result == null) {
logger.warn("Decoded result is null!");
} else {
logger.debug("Successfully decoded to object of type: {}", result.getClass().getName());
}
logger.debug("Returning result from decoder");
return result;
} catch (IOException e) {
logger.error("Error decoding JSON response. Status: {}, Raw body: {}", response.status(), json, e);
logger.error("IOException during decoding. Status: {}, Raw body: {}", response.status(), json, e);
throw new DecodeException(response.status(), "Error decoding JSON response", response.request(), e);
} catch (Exception e) {
logger.error("Unexpected error during decoding. Status: {}, Type: {}, Raw body: {}", response.status(), type, json, e);
throw new DecodeException(response.status(), "Unexpected error during decoding", response.request(), e);
}
}
};
Expand Down
Loading
Loading