diff --git a/client/pom.xml b/client/pom.xml
index 7118f455ab5f..9525412efc38 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -131,6 +131,16 @@
cloud-plugin-storage-volume-solidfire
${project.version}
+
+ org.apache.cloudstack
+ cloud-plugin-storage-volume-netapp
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-plugin-storage-image-netapp
+ ${project.version}
+
org.apache.cloudstack
cloud-plugin-storage-volume-cloudbyte
diff --git a/plugins/pom.xml b/plugins/pom.xml
index e4904ccdf40b..95badb239892 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -124,9 +124,11 @@
storage/image/s3
storage/image/sample
storage/image/swift
+ storage/image/netapp
storage/volume/cloudbyte
storage/volume/datera
storage/volume/default
+ storage/volume/netapp
storage/volume/nexenta
storage/volume/sample
storage/volume/ontap
diff --git a/plugins/storage/image/netapp/pom.xml b/plugins/storage/image/netapp/pom.xml
new file mode 100644
index 000000000000..4e6109b0a98c
--- /dev/null
+++ b/plugins/storage/image/netapp/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+ cloud-plugin-storage-image-netapp
+ Apache CloudStack Plugin - Storage Image NetApp Provider
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.21.0.0-SNAPSHOT
+ ../../../pom.xml
+
+
+
+ org.apache.cloudstack
+ cloud-plugin-storage-image-default
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-engine-storage-image
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ integration-test
+
+ test
+
+
+
+
+
+
+
diff --git a/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppSecondaryDatastoreDriver.java b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppSecondaryDatastoreDriver.java
new file mode 100644
index 000000000000..65f2d74acc22
--- /dev/null
+++ b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppSecondaryDatastoreDriver.java
@@ -0,0 +1,91 @@
+package org.apache.cloudstack.storage.driver;
+
+
+import com.cloud.agent.api.to.DataStoreTO;
+import com.cloud.agent.api.to.DataTO;
+import com.cloud.agent.api.to.DatadiskTO;
+import com.cloud.host.Host;
+import com.cloud.storage.Storage;
+import com.cloud.storage.Upload;
+import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
+import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
+import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.storage.image.ImageStoreDriver;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class NetAppSecondaryDatastoreDriver implements ImageStoreDriver {
+
+ @Override
+ public Map getCapabilities() {
+ return null;
+ }
+
+ @Override
+ public DataTO getTO(DataObject data) {
+ return null;
+ }
+
+ @Override
+ public DataStoreTO getStoreTO(DataStore store) {
+ return null;
+ }
+
+ @Override
+ public void createAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) {
+
+ }
+
+
+ @Override
+ public boolean canCopy(DataObject srcData, DataObject destData) {
+ return false;
+ }
+
+ @Override
+ public void resize(DataObject data, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public String createEntityExtractUrl(DataStore store, String installPath, Storage.ImageFormat format, DataObject dataObject) {
+ return null;
+ }
+
+ @Override
+ public void deleteEntityExtractUrl(DataStore store, String installPath, String url, Upload.Type entityType) {
+
+ }
+
+ @Override
+ public List getDataDiskTemplates(DataObject obj, String configurationId) {
+ return null;
+ }
+
+ @Override
+ public Void createDataDiskTemplateAsync(TemplateInfo dataDiskTemplate, String path, String diskId, boolean bootable, long fileSize, AsyncCompletionCallback callback) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppSecondaryDatastoreLifecycle.java b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppSecondaryDatastoreLifecycle.java
new file mode 100644
index 000000000000..3227af8cfb64
--- /dev/null
+++ b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppSecondaryDatastoreLifecycle.java
@@ -0,0 +1,60 @@
+package org.apache.cloudstack.storage.lifecycle;
+
+
+import com.cloud.agent.api.StoragePoolInfo;
+import com.cloud.hypervisor.Hypervisor;
+import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.HostScope;
+import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
+import org.apache.cloudstack.storage.image.store.lifecycle.ImageStoreLifeCycle;
+
+import java.util.Map;
+
+public class NetAppSecondaryDatastoreLifecycle implements ImageStoreLifeCycle {
+
+ /**
+ * Creates secondary storage on NetApp storage
+ * @param dsInfos
+ * @return
+ */
+ @Override
+ public DataStore initialize(Map dsInfos) {
+ return null;
+ }
+
+ @Override
+ public boolean attachCluster(DataStore store, ClusterScope scope) {
+ return false;
+ }
+
+ @Override
+ public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) {
+ return false;
+ }
+
+ @Override
+ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) {
+ return true;
+ }
+
+ @Override
+ public boolean maintain(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean cancelMaintain(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean deleteDataStore(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean migrateToObjectStore(DataStore store) {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppSecondaryDatastoreProvider.java b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppSecondaryDatastoreProvider.java
new file mode 100644
index 000000000000..0b880f59a92c
--- /dev/null
+++ b/plugins/storage/image/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppSecondaryDatastoreProvider.java
@@ -0,0 +1,79 @@
+package org.apache.cloudstack.storage.provider;
+
+import com.cloud.storage.ScopeType;
+import com.cloud.utils.component.ComponentContext;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle;
+import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
+import org.apache.cloudstack.engine.subsystem.api.storage.ImageStoreProvider;
+import org.apache.cloudstack.storage.driver.NetAppSecondaryDatastoreDriver;
+import org.apache.cloudstack.storage.image.datastore.ImageStoreProviderManager;
+import org.apache.cloudstack.storage.lifecycle.NetAppSecondaryDatastoreLifecycle;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@Component
+public class NetAppSecondaryDatastoreProvider implements ImageStoreProvider {
+
+ private static final Logger s_logger = (Logger)LogManager.getLogger(NetAppSecondaryDatastoreProvider.class);
+ @Inject
+ ImageStoreProviderManager storeMgr;
+ private NetAppSecondaryDatastoreDriver secondaryDatastoreDriver;
+ private NetAppSecondaryDatastoreLifecycle secondaryDatastoreLifecycle;
+
+ public NetAppSecondaryDatastoreProvider() {
+ s_logger.info("NetAppSecondaryDatastoreProvider initialized");
+ }
+ @Override
+ public DataStoreLifeCycle getDataStoreLifeCycle() {
+ return secondaryDatastoreLifecycle;
+ }
+
+ @Override
+ public DataStoreDriver getDataStoreDriver() {
+ return secondaryDatastoreDriver;
+ }
+
+ @Override
+ public HypervisorHostListener getHostListener() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "NetApp";
+ }
+
+ @Override
+ public boolean configure(Map params) {
+ secondaryDatastoreLifecycle = ComponentContext.inject(NetAppSecondaryDatastoreLifecycle.class);
+ secondaryDatastoreDriver = ComponentContext.inject(NetAppSecondaryDatastoreDriver.class);
+ storeMgr.registerDriver(this.getName(), secondaryDatastoreDriver);
+ return true;
+ }
+
+ @Override
+ public Set getTypes() {
+ Set types = new HashSet();
+ types.add(DataStoreProviderType.IMAGE);
+ return types;
+ }
+
+ @Override
+ public boolean isScopeSupported(ScopeType scope) {
+ if (scope == ScopeType.REGION)
+ return true;
+ return false;
+ }
+
+ @Override
+ public boolean needDownloadSysTemplate() {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/module.properties b/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/module.properties
new file mode 100644
index 000000000000..f068e552090c
--- /dev/null
+++ b/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/module.properties
@@ -0,0 +1,18 @@
+# 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.
+name=storage-image-netapp
+parent=storage
diff --git a/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/spring-storage-image-netapp-context.xml b/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/spring-storage-image-netapp-context.xml
new file mode 100644
index 000000000000..c268c5a6adc1
--- /dev/null
+++ b/plugins/storage/image/netapp/src/main/resources/META-INF/cloudstack/storage-image-netapp/spring-storage-image-netapp-context.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/plugins/storage/volume/netapp/pom.xml b/plugins/storage/volume/netapp/pom.xml
new file mode 100644
index 000000000000..7762a4354401
--- /dev/null
+++ b/plugins/storage/volume/netapp/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+ cloud-plugin-storage-volume-netapp
+ Apache CloudStack Plugin - Storage Volume NetApp Provider
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.21.0.0-SNAPSHOT
+ ../../../pom.xml
+
+
+
+ org.apache.cloudstack
+ cloud-plugin-storage-volume-default
+ ${project.version}
+
+
+ org.json
+ json
+ 20230227
+
+
+ org.apache.cloudstack
+ cloud-engine-storage-volume
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ integration-test
+
+ test
+
+
+
+
+
+
+
diff --git a/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppPrimaryDatastoreDriver.java b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppPrimaryDatastoreDriver.java
new file mode 100644
index 000000000000..ccc38fc75fd4
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/driver/NetAppPrimaryDatastoreDriver.java
@@ -0,0 +1,192 @@
+package org.apache.cloudstack.storage.driver;
+
+
+import com.cloud.agent.api.to.DataStoreTO;
+import com.cloud.agent.api.to.DataTO;
+import com.cloud.host.Host;
+import com.cloud.storage.Storage;
+import com.cloud.storage.StoragePool;
+import com.cloud.storage.Volume;
+import com.cloud.utils.Pair;
+import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
+import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
+import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class NetAppPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
+
+ private static final Logger s_logger = (Logger)LogManager.getLogger(NetAppPrimaryDatastoreDriver.class);
+ @Override
+ public Map getCapabilities() {
+ s_logger.trace("NetAppPrimaryDatastoreDriver: getCapabilities: Called");
+ Map 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());
+ mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString());
+ mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString());
+
+ return mapCapabilities;
+ }
+
+ @Override
+ public DataTO getTO(DataObject data) {
+ return null;
+ }
+
+ @Override
+ public DataStoreTO getStoreTO(DataStore store) {
+ return null;
+ }
+
+ @Override
+ public void createAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) {
+
+ s_logger.trace("NetAppPrimaryDatastoreDriver: createAsync: Store: "+store+", data: "+data);
+ }
+
+ @Override
+ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public boolean canCopy(DataObject srcData, DataObject destData) {
+ return false;
+ }
+
+ @Override
+ public void resize(DataObject data, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public ChapInfo getChapInfo(DataObject dataObject) {
+ return null;
+ }
+
+ @Override
+ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore) {
+ return true;
+ }
+
+ @Override
+ public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) {
+
+ }
+
+ @Override
+ public long getDataObjectSizeIncludingHypervisorSnapshotReserve(DataObject dataObject, StoragePool storagePool) {
+ return 0;
+ }
+
+ @Override
+ public long getBytesRequiredForTemplate(TemplateInfo templateInfo, StoragePool storagePool) {
+ return 0;
+ }
+
+ @Override
+ public long getUsedBytes(StoragePool storagePool) {
+ return 0;
+ }
+
+ @Override
+ public long getUsedIops(StoragePool storagePool) {
+ return 0;
+ }
+
+ @Override
+ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snapshotOnPrimaryStore, AsyncCompletionCallback callback) {
+
+ }
+
+ @Override
+ public void handleQualityOfServiceForVolumeMigration(VolumeInfo volumeInfo, QualityOfServiceState qualityOfServiceState) {
+
+ }
+
+ @Override
+ public boolean canProvideStorageStats() {
+ return true;
+ }
+
+ @Override
+ public Pair getStorageStats(StoragePool storagePool) {
+ return null;
+ }
+
+ @Override
+ public boolean canProvideVolumeStats() {
+ return true;
+ }
+
+ @Override
+ public Pair getVolumeStats(StoragePool storagePool, String volumeId) {
+ return null;
+ }
+
+ @Override
+ public boolean canHostAccessStoragePool(Host host, StoragePool pool) {
+ return true;
+ }
+
+ @Override
+ public boolean isVmInfoNeeded() {
+ return true;
+ }
+
+ @Override
+ public void provideVmInfo(long vmId, long volumeId) {
+
+ }
+
+ @Override
+ public boolean isVmTagsNeeded(String tagKey) {
+ return true;
+ }
+
+ @Override
+ public void provideVmTags(long vmId, long volumeId, String tagValue) {
+
+ }
+
+ @Override
+ public boolean isStorageSupportHA(Storage.StoragePoolType type) {
+ return true;
+ }
+
+ @Override
+ public void detachVolumeFromAllStorageNodes(Volume volume) {
+
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppPrimaryDatastoreLifecycle.java b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppPrimaryDatastoreLifecycle.java
new file mode 100644
index 000000000000..fc04fe1d2ecd
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/lifecycle/NetAppPrimaryDatastoreLifecycle.java
@@ -0,0 +1,173 @@
+package org.apache.cloudstack.storage.lifecycle;
+
+
+import com.cloud.agent.api.StoragePoolInfo;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.Storage;
+import com.cloud.storage.StoragePool;
+import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.HostScope;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters;
+import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
+import org.apache.cloudstack.storage.util.NetAppUtil;
+import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+import javax.inject.Inject;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class NetAppPrimaryDatastoreLifecycle implements PrimaryDataStoreLifeCycle {
+
+ private static final Logger s_logger = (Logger)LogManager.getLogger(NetAppPrimaryDatastoreLifecycle.class);
+
+ @Inject
+ private PrimaryDataStoreHelper primaryDataStoreHelper;
+ NetAppUtil util = new NetAppUtil();
+ /**
+ * Creates primary storage on NetApp storage
+ * @param dsInfos
+ * @return
+ */
+ @Override
+ public DataStore initialize(Map dsInfos) {
+ s_logger.info("NetAppPrimaryDatastoreLifecycle: initialize: params: dsInfos: "+ dsInfos);
+ String url = (String)dsInfos.get("url");
+ Long zoneId = (Long)dsInfos.get("zoneId");
+ Long podId = (Long)dsInfos.get("podId");
+ Long clusterId = (Long)dsInfos.get("clusterId");
+ String storagePoolName = (String)dsInfos.get("name");
+ String providerName = (String)dsInfos.get("providerName");
+ Long capacityBytes = (Long)dsInfos.get("capacityBytes");
+ Long capacityIops = (Long)dsInfos.get("capacityIops");
+ String tags = (String)dsInfos.get("tags");
+ Boolean isTagARule = (Boolean) dsInfos.get("isTagARule");
+ Map details = (Map)dsInfos.get("details");
+
+ s_logger.info("values: "+dsInfos.toString());
+
+ if (podId == null ^ clusterId == null) {
+ throw new CloudRuntimeException("Both POD and cluster values together should either be specified or not.");
+ }
+
+ if (url == null || url.trim().equals("")) {
+ throw new IllegalArgumentException("Valid 'URL' value must be present.");
+ }
+
+ if (capacityBytes == null || capacityBytes <= 0) {
+ throw new IllegalArgumentException("'capacityBytes' must be present and greater than 0.");
+ }
+
+ if (capacityIops == null || capacityIops <= 0) {
+ throw new IllegalArgumentException("'capacityIops' must be present and greater than 0.");
+ }
+
+
+ CompletableFuture volumeCreatedFuture = util.createOntapVolume(url, storagePoolName, capacityBytes, details);
+ volumeCreatedFuture.thenAccept(volumeCreated -> {
+ if (volumeCreated) {
+ s_logger.info("Volume created");
+ } else {
+ throw new RuntimeException("Volume creation failed.");
+ }
+ });
+
+
+ PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters();
+
+ String input = "{ \"aggregates\": [ { \"name\": \"sti246_vsim_ocvs040a_aggr1\" } ], \"name\": \"vol_test2\", \"size\": 1024000, \"svm\": { \"name\": \"vs0\" }}";
+
+
+ // this adds a row in the cloud.storage_pool table for this SolidFire cluster
+ parameters.setCapacityBytes(10240000);
+ parameters.setHost("10.196.38.171");
+ // parameters.setPort();
+ parameters.setPath("https://10.196.38.171/");
+ parameters.setType(Storage.StoragePoolType.Iscsi);
+ parameters.setUuid(UUID.randomUUID().toString());
+ parameters.setZoneId(zoneId);
+ parameters.setPodId(podId);
+ parameters.setClusterId(clusterId);
+ parameters.setName(storagePoolName);
+ parameters.setProviderName(providerName);
+ parameters.setManaged(true);
+ parameters.setCapacityBytes(capacityBytes);
+ parameters.setUsedBytes(0);
+ parameters.setCapacityIops(capacityIops);
+ parameters.setHypervisorType(Hypervisor.HypervisorType.KVM);
+ parameters.setTags(tags);
+ parameters.setIsTagARule(isTagARule);
+ parameters.setDetails(details);
+ details.put("username", "admin");
+ details.put("password", "netapp1!");
+
+ return primaryDataStoreHelper.createPrimaryDataStore(parameters);
+
+ }
+
+ @Override
+ public boolean attachCluster(DataStore store, ClusterScope scope) {
+ return false;
+ }
+
+ @Override
+ public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) {
+ return false;
+ }
+
+ @Override
+ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) {
+ return false;
+ }
+
+ @Override
+ public boolean maintain(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean cancelMaintain(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean deleteDataStore(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public boolean migrateToObjectStore(DataStore store) {
+ return true;
+ }
+
+ @Override
+ public void updateStoragePool(StoragePool storagePool, Map details) {
+
+ }
+
+ @Override
+ public void enableStoragePool(DataStore store) {
+
+ }
+
+ @Override
+ public void disableStoragePool(DataStore store) {
+
+ }
+
+ @Override
+ public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, Hypervisor.HypervisorType hypervisorType) {
+
+ }
+
+ @Override
+ public void changeStoragePoolScopeToCluster(DataStore store, ClusterScope clusterScope, Hypervisor.HypervisorType hypervisorType) {
+
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/ontap/Volume.java b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/ontap/Volume.java
new file mode 100644
index 000000000000..d7b008d16019
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/ontap/Volume.java
@@ -0,0 +1,25 @@
+package org.apache.cloudstack.storage.ontap;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Volume {
+ @JsonProperty("name")
+ private Boolean name;
+
+ @JsonProperty("size")
+ private long size;
+
+ @JsonProperty("qos")
+ private long qos;
+
+ @JsonProperty("svmName")
+ private String svmName;
+
+ @JsonProperty("cluster")
+ private String cluster;
+
+}
diff --git a/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppPrimaryDatastoreProvider.java b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppPrimaryDatastoreProvider.java
new file mode 100644
index 000000000000..b433a8080123
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/provider/NetAppPrimaryDatastoreProvider.java
@@ -0,0 +1,65 @@
+package org.apache.cloudstack.storage.provider;
+
+
+import com.cloud.utils.component.ComponentContext;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle;
+import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreProvider;
+import org.apache.cloudstack.storage.driver.NetAppPrimaryDatastoreDriver;
+import org.apache.cloudstack.storage.lifecycle.NetAppPrimaryDatastoreLifecycle;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Logger;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@Component
+public class NetAppPrimaryDatastoreProvider implements PrimaryDataStoreProvider {
+
+ private static final Logger s_logger = (Logger)LogManager.getLogger(NetAppPrimaryDatastoreProvider.class);
+ private NetAppPrimaryDatastoreDriver primaryDatastoreDriver;
+ private NetAppPrimaryDatastoreLifecycle primaryDatastoreLifecycle;
+
+ public NetAppPrimaryDatastoreProvider() {
+ s_logger.info("NetAppPrimaryDatastoreProvider initialized");
+ }
+ @Override
+ public DataStoreLifeCycle getDataStoreLifeCycle() {
+ return primaryDatastoreLifecycle;
+ }
+
+ @Override
+ public DataStoreDriver getDataStoreDriver() {
+ return primaryDatastoreDriver;
+ }
+
+ @Override
+ public HypervisorHostListener getHostListener() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ s_logger.trace("NetAppPrimaryDatastoreProvider: getName: Called");
+ return "NetAppStorage";
+ }
+
+ @Override
+ public boolean configure(Map params) {
+ s_logger.trace("NetAppPrimaryDatastoreProvider: configure: Called");
+ primaryDatastoreDriver = ComponentContext.inject(NetAppPrimaryDatastoreDriver.class);
+ primaryDatastoreLifecycle = ComponentContext.inject(NetAppPrimaryDatastoreLifecycle.class);
+ return true;
+ }
+
+ @Override
+ public Set getTypes() {
+ s_logger.trace("NetAppPrimaryDatastoreProvider: getTypes: Called");
+ Set typeSet = new HashSet();
+ typeSet.add(DataStoreProviderType.PRIMARY);
+ return typeSet;
+ }
+}
\ No newline at end of file
diff --git a/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/util/NetAppUtil.java b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/util/NetAppUtil.java
new file mode 100644
index 000000000000..77152cf8de6b
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/java/org/apache/cloudstack/storage/util/NetAppUtil.java
@@ -0,0 +1,67 @@
+package org.apache.cloudstack.storage.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.CompletableFuture;
+
+import java.util.Map;
+
+public class NetAppUtil {
+
+ private static final Logger s_logger = (Logger) LogManager.getLogger(NetAppUtil.class);
+ private final long connTimeout = 5000;
+ private final boolean skipTlsValidation = false;
+ static final ObjectMapper mapper = new ObjectMapper();
+ // Use shared HttpClient instance
+ HttpClient client = HttpClient.newHttpClient();
+
+ public CompletableFuture createOntapVolume(String url, String volumeName, Long capacityBytes, Map details) {
+ HttpRequest request = null;
+ try {
+ String payload1 = "{ \"aggregates\": [ " +
+ " { " +
+ " \"name\": \"sti246_vsim_ocvs040a_aggr1\" " +
+ " } " +
+ " ], " +
+ " \"name\": \"" + volumeName + "\", " +
+ " \"size\": " + capacityBytes + ", " +
+ " \"svm\": { " +
+ " \"name\": \"vs0\" " +
+ " }" +
+ " }";
+
+ // Prepare payload
+ /*Map payload = Map.of(
+ "name", volumeName,
+ "size", capacityBytes,
+ "details", details
+ );*/
+
+ ObjectMapper mapper = new ObjectMapper();
+ //String jsonPayload = mapper.writeValueAsString(payload);
+
+ // Build HTTP request
+ request = HttpRequest.newBuilder()
+ .uri(URI.create("https://10.196.38.171/api/storage/volumes"))
+ .header("Content-Type", "application/json")
+ .header("username", "admin")
+ .header("password", "netapp1!")
+ .POST(HttpRequest.BodyPublishers.ofString(payload1))
+ .build();
+
+ // Send async request
+ return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
+ .thenApply(response -> response.statusCode() == 200 || response.statusCode() == 201);
+ } catch (Exception e) {
+ CompletableFuture failedFuture = new CompletableFuture<>();
+ failedFuture.completeExceptionally(e);
+ return failedFuture;
+ }
+ }
+}
+
diff --git a/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/module.properties b/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/module.properties
new file mode 100644
index 000000000000..7f3779bb5eb9
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/module.properties
@@ -0,0 +1,18 @@
+# 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.
+name=storage-volume-netapp
+parent=storage
diff --git a/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/spring-storage-volume-netapp-context.xml b/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/spring-storage-volume-netapp-context.xml
new file mode 100644
index 000000000000..25bb004ca6fd
--- /dev/null
+++ b/plugins/storage/volume/netapp/src/main/resources/META-INF/cloudstack/storage-volume-netapp/spring-storage-volume-netapp-context.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index 9ab57dc60a62..7dc75538a01c 100755
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -31,6 +31,8 @@
import com.cloud.storage.ScopeType;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.storage.dao.SnapshotDetailsDao;
+import com.cloud.storage.dao.SnapshotDetailsVO;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
@@ -47,18 +49,22 @@
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.cloudstack.storage.command.CreateObjectAnswer;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.service.SANStrategy;
+import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.service.StorageStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
+import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.utils.Constants;
import org.apache.cloudstack.storage.utils.Utility;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -80,6 +86,8 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
@Inject private VMInstanceDao vmDao;
@Inject private VolumeDao volumeDao;
@Inject private VolumeDetailsDao volumeDetailsDao;
+ @Inject private SnapshotDetailsDao snapshotDetailsDao;
+
@Override
public Map getCapabilities() {
s_logger.trace("OntapPrimaryDatastoreDriver: getCapabilities: Called");
@@ -88,6 +96,7 @@ public Map getCapabilities() {
// TODO Set it to false once we start supporting snapshot feature
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.FALSE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.FALSE.toString());
+
return mapCapabilities;
}
@@ -254,6 +263,7 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac
@Override
public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) {
+
}
@Override
@@ -525,6 +535,81 @@ public long getUsedIops(StoragePool storagePool) {
@Override
public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) {
+ CreateCmdResult result;
+
+ try {
+ VolumeInfo volumeInfo = snapshot.getBaseVolume();
+
+ VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId());
+ if(volumeVO == null) {
+ throw new CloudRuntimeException("takeSnapshot: VolumeVO not found for id: " + volumeInfo.getId());
+ }
+
+ /** we are keeping file path at volumeVO.getPath() */
+
+ StoragePoolVO storagePool = storagePoolDao.findById(volumeVO.getPoolId());
+ if(storagePool == null) {
+ s_logger.error("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId());
+ throw new CloudRuntimeException("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId());
+ }
+ Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId());
+ StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(poolDetails);
+
+ Map cloudStackVolumeRequestMap = new HashMap<>();
+ cloudStackVolumeRequestMap.put(Constants.VOLUME_UUID, poolDetails.get(Constants.VOLUME_UUID));
+ cloudStackVolumeRequestMap.put(Constants.FILE_PATH, volumeVO.getPath());
+ CloudStackVolume cloudStackVolume = storageStrategy.getCloudStackVolume(cloudStackVolumeRequestMap);
+ if (cloudStackVolume == null || cloudStackVolume.getFile() == null) {
+ throw new CloudRuntimeException("takeSnapshot: Failed to get source file to take snapshot");
+ }
+ long capacityBytes = storagePool.getCapacityBytes();
+
+ long usedBytes = getUsedBytes(storagePool);
+ long fileSize = cloudStackVolume.getFile().getSize();
+
+ usedBytes += fileSize;
+
+ if (usedBytes > capacityBytes) {
+ throw new CloudRuntimeException("Insufficient space remains in this primary storage to take a snapshot");
+ }
+
+ storagePool.setUsedBytes(usedBytes);
+
+ SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO)snapshot.getTO();
+
+ String fileSnapshotName = volumeInfo.getName() + "-" + snapshot.getUuid();
+
+ int maxSnapshotNameLength = 64;
+ int trimRequired = fileSnapshotName.length() - maxSnapshotNameLength;
+
+ if (trimRequired > 0) {
+ fileSnapshotName = StringUtils.left(volumeInfo.getName(), (volumeInfo.getName().length() - trimRequired)) + "-" + snapshot.getUuid();
+ }
+
+ CloudStackVolume snapCloudStackVolumeRequest = snapshotCloudStackVolumeRequestByProtocol(poolDetails, volumeVO.getPath(), fileSnapshotName);
+ CloudStackVolume cloneCloudStackVolume = storageStrategy.snapshotCloudStackVolume(snapCloudStackVolumeRequest);
+
+ updateSnapshotDetails(snapshot.getId(), volumeInfo.getId(), poolDetails.get(Constants.VOLUME_UUID), cloneCloudStackVolume.getFile().getPath(), volumeVO.getPoolId(), fileSize);
+
+ snapshotObjectTo.setPath(Constants.ONTAP_SNAP_ID +"="+cloneCloudStackVolume.getFile().getPath());
+
+ /** Update size for the storage-pool including snapshot size */
+ storagePoolDao.update(volumeVO.getPoolId(), storagePool);
+
+ CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo);
+
+ result = new CreateCmdResult(null, createObjectAnswer);
+
+ result.setResult(null);
+ }
+ catch (Exception ex) {
+ s_logger.error("takeSnapshot: Failed due to ", ex);
+ result = new CreateCmdResult(null, new CreateObjectAnswer(ex.toString()));
+
+ result.setResult(ex.toString());
+ }
+
+ callback.complete(result);
}
@Override
@@ -622,4 +707,87 @@ private CloudStackVolume createDeleteCloudStackVolumeRequest(StoragePool storage
return cloudStackVolumeDeleteRequest;
}
+
+
+ private CloudStackVolume getCloudStackVolumeRequestByProtocol(Map details, String filePath) {
+ CloudStackVolume cloudStackVolumeRequest = null;
+ ProtocolType protocolType = null;
+ String protocol = null;
+
+ try {
+ protocol = details.get(Constants.PROTOCOL);
+ protocolType = ProtocolType.valueOf(protocol);
+ } catch (IllegalArgumentException e) {
+ throw new CloudRuntimeException("getCloudStackVolumeRequestByProtocol: Protocol: "+ protocol +" is not valid");
+ }
+ switch (protocolType) {
+ case NFS3:
+ cloudStackVolumeRequest = new CloudStackVolume();
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setPath(filePath);
+ cloudStackVolumeRequest.setFile(fileInfo);
+ String volumeUuid = details.get(Constants.VOLUME_UUID);
+ cloudStackVolumeRequest.setFlexVolumeUuid(volumeUuid);
+ break;
+ default:
+ throw new CloudRuntimeException("createCloudStackVolumeRequestByProtocol: Unsupported protocol " + protocol);
+ }
+ return cloudStackVolumeRequest;
+ }
+
+private CloudStackVolume snapshotCloudStackVolumeRequestByProtocol(Map details,
+ String sourcePath,
+ String destinationPath) {
+ CloudStackVolume cloudStackVolumeRequest = null;
+ ProtocolType protocolType = null;
+ String protocol = null;
+
+ try {
+ protocol = details.get(Constants.PROTOCOL);
+ protocolType = ProtocolType.valueOf(protocol);
+ } catch (IllegalArgumentException e) {
+ throw new CloudRuntimeException("getCloudStackVolumeRequestByProtocol: Protocol: "+ protocol +" is not valid");
+ }
+ switch (protocolType) {
+ case NFS3:
+ cloudStackVolumeRequest = new CloudStackVolume();
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setPath(sourcePath);
+ cloudStackVolumeRequest.setFile(fileInfo);
+ String volumeUuid = details.get(Constants.VOLUME_UUID);
+ cloudStackVolumeRequest.setFlexVolumeUuid(volumeUuid);
+ cloudStackVolumeRequest.setDestinationPath(destinationPath);
+ break;
+ default:
+ throw new CloudRuntimeException("createCloudStackVolumeRequestByProtocol: Unsupported protocol " + protocol);
+
+ }
+ return cloudStackVolumeRequest;
+}
+
+ /**
+ *
+ * @param csSnapshotId: generated snapshot id from cloudstack
+ * @param csVolumeId: Source CS volume id
+ * @param ontapVolumeUuid: storage flexvolume id
+ * @param ontapNewSnapshot: generated snapshot id from ONTAP
+ * @param storagePoolId: primary storage pool id
+ * @param ontapSnapSize: Size of snapshot CS volume(LUN/file)
+ */
+ private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String ontapVolumeUuid, String ontapNewSnapshot, long storagePoolId, long ontapSnapSize) {
+ SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.SRC_CS_VOLUME_ID, String.valueOf(csVolumeId), false);
+ snapshotDetailsDao.persist(snapshotDetail);
+
+ snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.BASE_ONTAP_FV_ID, String.valueOf(ontapVolumeUuid), false);
+ snapshotDetailsDao.persist(snapshotDetail);
+
+ snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_ID, String.valueOf(ontapNewSnapshot), false);
+ snapshotDetailsDao.persist(snapshotDetail);
+
+ snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.PRIMARY_POOL_ID, String.valueOf(storagePoolId), false);
+ snapshotDetailsDao.persist(snapshotDetail);
+
+ snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_SIZE, String.valueOf(ontapSnapSize), false);
+ snapshotDetailsDao.persist(snapshotDetail);
+}
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
index f48f83dc28de..8d4df6d8c4f1 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
@@ -21,7 +21,9 @@
import feign.QueryMap;
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
+import org.apache.cloudstack.storage.feign.model.FileClone;
import org.apache.cloudstack.storage.feign.model.FileInfo;
+import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import feign.Headers;
import feign.Param;
@@ -58,6 +60,11 @@ void createFile(@Param("authHeader") String authHeader,
@Param("path") String filePath,
FileInfo file);
+ @RequestLine("POST /api/storage/volumes/{volumeUuid}/files/{path}")
+ @Headers({"Authorization: {authHeader}"})
+ JobResponse cloneFile(@Param("authHeader") String authHeader,
+ FileClone fileClone);
+
// Export Policy Operations
@RequestLine("POST /api/protocols/nfs/export-policies")
@Headers({"Authorization: {authHeader}"})
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java
new file mode 100644
index 000000000000..a117ec6e6a0b
--- /dev/null
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java
@@ -0,0 +1,51 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class FileClone {
+ @JsonProperty("source_path")
+ private String sourcePath;
+ @JsonProperty("destination_path")
+ private String destinationPath;
+ @JsonProperty("volume")
+ private VolumeConcise volume;
+ public VolumeConcise getVolume() {
+ return volume;
+ }
+ public void setVolume(VolumeConcise volume) {
+ this.volume = volume;
+ }
+ public String getSourcePath() {
+ return sourcePath;
+ }
+ public void setSourcePath(String sourcePath) {
+ this.sourcePath = sourcePath;
+ }
+ public String getDestinationPath() {
+ return destinationPath;
+ }
+ public void setDestinationPath(String destinationPath) {}
+}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java
new file mode 100644
index 000000000000..eaa5b2ed2ae9
--- /dev/null
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java
@@ -0,0 +1,43 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class VolumeConcise {
+ @JsonProperty("uuid")
+ private String uuid;
+ @JsonProperty("name")
+ private String name;
+ public String getUuid() {
+ return uuid;
+ }
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {}
+}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
index d9f98dcf7cb1..8aa05ee46a7c 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
@@ -318,7 +318,7 @@ public void deleteStorageVolume(Volume volume) {
try {
// TODO: Implement lun and file deletion, if any, before deleting the volume
JobResponse jobResponse = volumeFeignClient.deleteVolume(authHeader, volume.getUuid());
- Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(), 10, 1);
+ Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(),5, 1);
if (!jobSucceeded) {
s_logger.error("Volume deletion job failed for volume: " + volume.getName());
throw new CloudRuntimeException("Volume deletion job failed for volume: " + volume.getName());
@@ -506,6 +506,19 @@ public String getNetworkInterface() {
*/
abstract public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap);
+
+ /**
+ * Method encapsulates the behavior based on the opted protocol in subclasses.
+ * it is going to mimic
+ * snapshotLun for iSCSI, FC protocols
+ * snapshotFile for NFS3.0 and NFS4.1 protocols
+ *
+ *
+ * @param cloudstackVolume the source CloudStack volume
+ * @return the retrieved snapshot CloudStackVolume object
+ */
+ public abstract CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolume);
+
/**
* Method encapsulates the behavior based on the opted protocol in subclasses
* createiGroup for iSCSI and FC protocols
@@ -569,7 +582,7 @@ public String getNetworkInterface() {
*/
abstract public Map getLogicalAccess(Map values);
- private Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeInSecs) {
+ protected Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeInSecs) {
//Create URI for GET Job API
int jobRetryCount = 0;
Job jobResp = null;
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
index c2aa4e462d2f..5faa38e61be3 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
@@ -39,12 +39,14 @@
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
import org.apache.cloudstack.storage.feign.model.ExportRule;
+import org.apache.cloudstack.storage.feign.model.FileClone;
import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.Volume;
+import org.apache.cloudstack.storage.feign.model.VolumeConcise;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -134,7 +136,72 @@ public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) {
@Override
public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap) {
- return null;
+ s_logger.info("getCloudStackVolume: Get cloudstack volume " + cloudStackVolumeMap);
+ CloudStackVolume cloudStackVolume = null;
+ FileInfo fileInfo = getFile(cloudStackVolumeMap.get(Constants.VOLUME_UUID),cloudStackVolumeMap.get(Constants.FILE_PATH));
+
+ if(fileInfo != null){
+ cloudStackVolume = new CloudStackVolume();
+ cloudStackVolume.setFlexVolumeUuid(cloudStackVolumeMap.get(Constants.VOLUME_UUID));
+ cloudStackVolume.setFile(fileInfo);
+ } else {
+ s_logger.warn("getCloudStackVolume: File not found for volume UUID: {} and file path: {}", cloudStackVolumeMap.get(Constants.VOLUME_UUID), cloudStackVolumeMap.get(Constants.FILE_PATH));
+ }
+
+ return cloudStackVolume;
+ }
+
+ @Override
+ public CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolumeArg) {
+ s_logger.info("snapshotCloudStackVolume: Get cloudstack volume " + cloudstackVolumeArg);
+ CloudStackVolume cloudStackVolume = null;
+ String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ JobResponse jobResponse = null;
+
+ FileClone fileClone = new FileClone();
+ VolumeConcise volumeConcise = new VolumeConcise();
+ volumeConcise.setUuid(cloudstackVolumeArg.getFlexVolumeUuid());
+ fileClone.setVolume(volumeConcise);
+
+ fileClone.setSourcePath(cloudstackVolumeArg.getFile().getPath());
+ fileClone.setDestinationPath(cloudstackVolumeArg.getDestinationPath());
+
+ try {
+ /** Clone file call to storage */
+ jobResponse = nasFeignClient.cloneFile(authHeader, fileClone);
+ if (jobResponse == null || jobResponse.getJob() == null) {
+ throw new CloudRuntimeException("Failed to initiate file clone" + cloudstackVolumeArg.getFile().getPath());
+ }
+ String jobUUID = jobResponse.getJob().getUuid();
+
+ /** Create URI for GET Job API */
+ Boolean jobSucceeded = jobPollForSuccess(jobUUID,3,2);
+ if (!jobSucceeded) {
+ s_logger.error("File clone failed: " + cloudstackVolumeArg.getFile().getPath());
+ throw new CloudRuntimeException("File clone failed: " + cloudstackVolumeArg.getFile().getPath());
+ }
+ s_logger.info("File clone job completed successfully for file: " + cloudstackVolumeArg.getFile().getPath());
+
+ } catch (FeignException e) {
+ s_logger.error("Failed to clone file response: " + cloudstackVolumeArg.getFile().getPath(), e);
+ throw new CloudRuntimeException("File not found: " + e.getMessage());
+ } catch (Exception e) {
+ s_logger.error("Exception to get file: {}", cloudstackVolumeArg.getFile().getPath(), e);
+ throw new CloudRuntimeException("Failed to get the file: " + e.getMessage());
+ }
+
+ FileInfo clonedFileInfo = null;
+ try {
+ /** Get cloned file call from storage */
+ clonedFileInfo = getFile(cloudstackVolumeArg.getFlexVolumeUuid(), cloudstackVolumeArg.getDestinationPath());
+ } catch (Exception e) {
+ s_logger.error("Exception to get cloned file: {}", cloudstackVolumeArg.getDestinationPath(), e);
+ throw new CloudRuntimeException("Failed to get the cloned file: " + e.getMessage());
+ }
+ cloudStackVolume = new CloudStackVolume();
+ cloudStackVolume.setFlexVolumeUuid(cloudstackVolumeArg.getFlexVolumeUuid());
+ cloudStackVolume.setFile(clonedFileInfo);
+ return cloudStackVolume;
}
@Override
@@ -436,10 +503,13 @@ private boolean updateFile(String volumeUuid, String filePath, FileInfo fileInfo
}
}
+ private String generateExportPolicyName(String svmName, String volumeName){
+ return Constants.EXPORT + Constants.HYPHEN + svmName + Constants.HYPHEN + volumeName;
+ }
private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String svmName , String volumeName){
- String exportPolicyName = Utility.generateExportPolicyName(svmName,volumeName);
+ String exportPolicyName = generateExportPolicyName(svmName,volumeName);
ExportPolicy exportPolicy = new ExportPolicy();
List rules = new ArrayList<>();
@@ -548,4 +618,26 @@ private Answer deleteVolumeOnKVMHost(DataObject volumeInfo) {
return new Answer(null, false, e.toString());
}
}
+
+ private FileInfo getFile(String volumeUuid, String filePath) {
+ s_logger.info("Get File: {} for volume: {}", filePath, volumeUuid);
+
+ String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ OntapResponse fileResponse = null;
+ try {
+ fileResponse = nasFeignClient.getFileResponse(authHeader, volumeUuid, filePath);
+ if (fileResponse == null || fileResponse.getRecords().isEmpty()) {
+ throw new CloudRuntimeException("File " + filePath + " not not found on ONTAP. " +
+ "Received successful response but file does not exist.");
+ }
+ } catch (FeignException e) {
+ s_logger.error("Failed to get file response: " + filePath, e);
+ throw new CloudRuntimeException("File not found: " + e.getMessage());
+ } catch (Exception e) {
+ s_logger.error("Exception to get file: {}", filePath, e);
+ throw new CloudRuntimeException("Failed to get the file: " + e.getMessage());
+ }
+ s_logger.info("File retrieved successfully with name {}", filePath);
+ return fileResponse.getRecords().get(0);
+ }
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
index c42e5cb6f516..e486ff249296 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
@@ -93,7 +93,7 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
cloudstackVolume.getLun().getName(), e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
} catch (Exception e) {
- s_logger.error("Exception occurred while creating LUN: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage());
+ s_logger.error("Exception occurred while creating LUN: {}. Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage());
throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
}
}
@@ -202,6 +202,11 @@ public CloudStackVolume getCloudStackVolume(Map values) {
}
}
+ @Override
+ public CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ return null;
+ }
+
@Override
public AccessGroup createAccessGroup(AccessGroup accessGroup) {
s_logger.info("createAccessGroup : Create Igroup");
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java
index 6c51e4630800..c62d093fe071 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java
@@ -25,9 +25,27 @@
public class CloudStackVolume {
+ /**
+ * Filed used for request:
+ * a. snapshot workflows will get source file details from it.
+ */
private FileInfo file;
+ /**
+ * Filed used for request:
+ * a. snapshot workflows will get source LUN details from it.
+ */
private Lun lun;
private String datastoreId;
+ /**
+ * FlexVolume UUID on which this cloudstack volume is created.
+ * a. Field is eligible for unified storage only.
+ * b. It will be null for the disaggregated storage.
+ */
+ private String flexVolumeUuid;
+ /**
+ * Field serves for snapshot workflows
+ */
+ private String destinationPath;
private DataObject volumeInfo; // This is needed as we need DataObject to be passed to agent to create volume
public FileInfo getFile() {
return file;
@@ -37,6 +55,10 @@ public void setFile(FileInfo file) {
this.file = file;
}
+ public String getDestinationPath() { return this.destinationPath; }
+
+ public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; }
+
public Lun getLun() {
return lun;
}
@@ -56,4 +78,10 @@ public DataObject getVolumeInfo() {
public void setVolumeInfo(DataObject volumeInfo) {
this.volumeInfo = volumeInfo;
}
+ public String getFlexVolumeUuid() {
+ return flexVolumeUuid;
+ }
+ public void setFlexVolumeUuid(String flexVolumeUuid) {
+ this.flexVolumeUuid = flexVolumeUuid;
+ }
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java
index 5e8729ad1917..2474ad2598b6 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java
@@ -90,4 +90,10 @@ public class Constants {
public static final String IGROUP_DOT_UUID = "igroup.uuid";
public static final String UNDERSCORE = "_";
public static final String CS = "cs";
+ public static final String SRC_CS_VOLUME_ID = "src_cs_volume_id";
+ public static final String BASE_ONTAP_FV_ID = "base_ontap_fv_id";
+ public static final String ONTAP_SNAP_ID = "ontap_snap_id";
+ public static final String PRIMARY_POOL_ID = "primary_pool_id";
+ public static final String ONTAP_SNAP_SIZE = "ontap_snap_size";
+ public static final String FILE_PATH = "file_path";
}
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java
index 467c01b2c995..f606cc7c04e6 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java
@@ -146,6 +146,11 @@ public CloudStackVolume getCloudStackVolume(
return null;
}
+ @Override
+ public CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ return null;
+ }
+
@Override
public AccessGroup createAccessGroup(
org.apache.cloudstack.storage.service.model.AccessGroup accessGroup) {
diff --git a/ui/public/locales/es.json b/ui/public/locales/es.json
index b7a409adad1e..889e9c3e6427 100644
--- a/ui/public/locales/es.json
+++ b/ui/public/locales/es.json
@@ -860,6 +860,9 @@
"label.presetup": "PreConfiguraci\u00f3n",
"label.prev": "Anterior",
"label.previous": "Previo",
+"label.netapp.username.tooltip": "The username with edit privileges",
+"label.netapp.password": "The password for the NetApp storage array",
+"label.netapp.url.tooltip": "URL designating the NetApp storage endpoint, formatted as: http[s]://HOSTNAME:PORT/",
"label.primary.storage": "Almacenamiento Primario",
"label.primary.storage.allocated": "Almacenamiento Primario Asignado",
"label.primary.storage.used": "Almacenamiento Primario Usado",
diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue
index a869ff0e6a1c..1536fc1fa937 100644
--- a/ui/src/views/infra/AddPrimaryStorage.vue
+++ b/ui/src/views/infra/AddPrimaryStorage.vue
@@ -242,7 +242,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -549,10 +581,9 @@ export default {
flashArrayURL: [{ required: true, message: this.$t('label.url') }],
flashArrayUsername: [{ required: true, message: this.$t('label.username') }],
flashArrayPassword: [{ required: true, message: this.$t('label.password') }],
- ontapIP: [{ required: true, message: this.$t('label.required') }],
- ontapUsername: [{ required: true, message: this.$t('label.required') }],
- ontapPassword: [{ required: true, message: this.$t('label.required') }],
- ontapSvmName: [{ required: true, message: this.$t('label.required') }]
+ netappURL: [{ required: true, message: this.$t('label.url') }],
+ netappUsername: [{ required: true, message: this.$t('label.username') }],
+ netappPassword: [{ required: true, message: this.$t('label.password') }]
})
},
fetchData () {