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 @@ -
+