From 9547f027523609fe1e2f60a200066c5a3f0e8472 Mon Sep 17 00:00:00 2001 From: "Locharla, Sandeep" Date: Tue, 21 Oct 2025 08:55:59 +0530 Subject: [PATCH 1/4] CSTACKEX-7: ONTAP Primary storage pool --- .../storage/feign/client/SvmFeignClient.java | 2 +- .../feign/client/VolumeFeignClient.java | 10 +- .../storage/feign/model/OntapStorage.java | 86 +++++++++ .../feign/model/response/OntapResponse.java | 6 +- .../OntapPrimaryDatastoreLifecycle.java | 159 +++++++++++++++- .../provider/StorageProviderFactory.java | 64 +++++++ .../storage/service/NASStrategy.java | 34 ++++ .../storage/service/SANStrategy.java | 33 ++++ .../storage/service/StorageStrategy.java | 173 ++++++++++++++++++ .../storage/service/UnifiedNASStrategy.java | 48 +++++ .../storage/service/UnifiedSANStrategy.java | 48 +++++ .../cloudstack/storage/utils/Constants.java | 46 +++++ .../cloudstack/storage/utils/Utility.java | 52 ++++++ 13 files changed, 747 insertions(+), 14 deletions(-) create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/NASStrategy.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java index 52ee30d71c8a..57c1cfb6b3ed 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java @@ -34,7 +34,7 @@ public interface SvmFeignClient { //this method to get all svms and also filtered svms based on query params as a part of URL @RequestMapping(method = RequestMethod.GET) - OntapResponse getSvmResponse(URI baseURL, @RequestHeader("Authorization") String header); + OntapResponse getSvms(URI baseURL, @RequestHeader("Authorization") String header); @RequestMapping(method = RequestMethod.GET, value = "/{uuid}") Svm getSvmByUUID(URI baseURL, @RequestHeader("Authorization") String header); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java index fb3c9712d750..af92754da42e 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java @@ -30,21 +30,23 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; +import java.net.URI; + @Lazy @FeignClient(name = "VolumeClient", url = "https://{clusterIP}/api/storage/volumes", configuration = FeignConfiguration.class) public interface VolumeFeignClient { @RequestMapping(method = RequestMethod.DELETE, value="/{uuid}") - void deleteVolume(@RequestHeader("Authorization") String authHeader, @PathVariable("uuid") String uuid); + void deleteVolume(URI baseURL, @RequestHeader("Authorization") String authHeader, @PathVariable("uuid") String uuid); @RequestMapping(method = RequestMethod.POST) - JobResponse createVolumeWithJob(@RequestHeader("Authorization") String authHeader, @RequestBody Volume volumeRequest); + JobResponse createVolumeWithJob(URI baseURL, @RequestHeader("Authorization") String authHeader, @RequestBody Volume volumeRequest); @RequestMapping(method = RequestMethod.GET, value="/{uuid}") - Volume getVolumeByUUID(@RequestHeader("Authorization") String authHeader, @PathVariable("uuid") String uuid); + Volume getVolumeByUUID(URI baseURL, @RequestHeader("Authorization") String authHeader, @PathVariable("uuid") String uuid); @RequestMapping(method = RequestMethod.PATCH) - JobResponse updateVolumeRebalancing(@RequestHeader("accept") String acceptHeader, @PathVariable("uuid") String uuid, @RequestBody Volume volumeRequest); + JobResponse updateVolumeRebalancing(URI baseURL, @RequestHeader("accept") String acceptHeader, @PathVariable("uuid") String uuid, @RequestBody Volume volumeRequest); } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java new file mode 100644 index 000000000000..d02112422f60 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java @@ -0,0 +1,86 @@ +/* + * 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; + +public class OntapStorage { + public static String Username; + public static String Password; + public static String ManagementLIF; + public static String Svm; + public static String Protocol; + public static Boolean IsDisaggregated; + + public OntapStorage(String username, String password, String managementLIF, String svm, String protocol, Boolean isDisaggregated) { + Username = username; + Password = password; + ManagementLIF = managementLIF; + Svm = svm; + Protocol = protocol; + IsDisaggregated = isDisaggregated; + } + + public String getUsername() { + return Username; + } + + public void setUsername(String username) { + Username = username; + } + + public String getPassword() { + return Password; + } + + public void setPassword(String password) { + Password = password; + } + + public String getManagementLIF() { + return ManagementLIF; + } + + public void setManagementLIF(String managementLIF) { + ManagementLIF = managementLIF; + } + + public String getSVM() { + return Svm; + } + + public void setSVM(String svm) { + Svm = svm; + } + + public String getProtocol() { + return Protocol; + } + + public void setProtocol(String protocol) { + Protocol = protocol; + } + + public Boolean getIsDisaggregated() { + return IsDisaggregated; + } + + public void setIsDisaggregated(Boolean isDisaggregated) { + IsDisaggregated = isDisaggregated; + } +} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java index 933c1ec0bdf4..b78f41e7df3b 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java @@ -24,7 +24,7 @@ import java.util.List; /** - * OnTapResponse + * OntapResponse */ @JsonInclude(JsonInclude.Include.NON_NULL) public class OntapResponse { @@ -34,11 +34,11 @@ public class OntapResponse { @JsonProperty("records") private List records; - public OntapResponse () { + public OntapResponse() { // Default constructor } - public OntapResponse (List records) { + public OntapResponse(List records) { this.records = records; this.numRecords = (records != null) ? records.size() : 0; } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java index 2aad3a3e8029..cc7eb5618f1a 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java @@ -21,19 +21,42 @@ import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.Storage; +import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.common.base.Preconditions; 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.PrimaryDataStoreInfo; 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.datastore.lifecycle.BasePrimaryDataStoreLifeCycleImpl; +import org.apache.cloudstack.storage.feign.model.OntapStorage; +import org.apache.cloudstack.storage.provider.StorageProviderFactory; +import org.apache.cloudstack.storage.service.StorageStrategy; +import org.apache.cloudstack.storage.utils.Constants; +import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.Map; -public class OntapPrimaryDatastoreLifecycle implements PrimaryDataStoreLifeCycle { +import javax.inject.Inject; +import java.util.List; +import java.util.Map; +import java.util.UUID; +public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { + @Inject private ClusterDao _clusterDao; + @Inject private StorageManager _storageMgr; + @Inject private ResourceManager _resourceMgr; + @Inject private PrimaryDataStoreHelper _dataStoreHelper; private static final Logger s_logger = (Logger)LogManager.getLogger(OntapPrimaryDatastoreLifecycle.class); /** @@ -43,14 +66,125 @@ public class OntapPrimaryDatastoreLifecycle implements PrimaryDataStoreLifeCycle */ @Override public DataStore initialize(Map dsInfos) { + if (dsInfos == null) { + throw new CloudRuntimeException("Datastore info map is null, cannot create primary storage"); + } + String url = dsInfos.get("url").toString(); // TODO: Decide on whether should the customer enter just the Management LIF IP or https://ManagementLIF + Long zoneId = (Long) dsInfos.get("zoneId"); + Long podId = (Long)dsInfos.get("podId"); + Long clusterId = (Long)dsInfos.get("clusterId"); + String storagePoolName = dsInfos.get("name").toString(); + String providerName = dsInfos.get("providerName").toString(); + String tags = dsInfos.get("tags").toString(); + Boolean isTagARule = (Boolean) dsInfos.get("isTagARule"); + String scheme = dsInfos.get("scheme").toString(); + + s_logger.info("Creating ONTAP primary storage pool with name: " + storagePoolName + ", provider: " + providerName + + ", zoneId: " + zoneId + ", podId: " + podId + ", clusterId: " + clusterId + ", protocol: " + scheme); + + // Additional details requested for ONTAP primary storage pool creation + @SuppressWarnings("unchecked") + Map details = (Map)dsInfos.get("details"); + // Validations + if (podId != null && clusterId == null) { + s_logger.error("Cluster Id is null, cannot create primary storage"); + return null; + } else if (podId == null && clusterId != null) { + s_logger.error("Pod Id is null, cannot create primary storage"); + return null; + } + + if (podId == null && clusterId == null) { + if (zoneId != null) { + s_logger.info("Both Pod Id and Cluster Id are null, Primary storage pool will be associated with a Zone"); + } else { + throw new CloudRuntimeException("Pod Id, Cluster Id and Zone Id are all null, cannot create primary storage"); + } + } + + if (storagePoolName == null || storagePoolName.isEmpty()) { + throw new CloudRuntimeException("Storage pool name is null or empty, cannot create primary storage"); + } + + if (providerName == null || providerName.isEmpty()) { + throw new CloudRuntimeException("Provider name is null or empty, cannot create primary storage"); + } + + PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters(); + if (clusterId != null) { + ClusterVO clusterVO = _clusterDao.findById(clusterId); + Preconditions.checkNotNull(clusterVO, "Unable to locate the specified cluster"); + if (clusterVO.getHypervisorType() != Hypervisor.HypervisorType.KVM) { + throw new CloudRuntimeException("ONTAP primary storage is not supported for KVM hypervisor"); + } + parameters.setHypervisorType(clusterVO.getHypervisorType()); + } + + // TODO: While testing need to check what does this actually do and if the fields corresponding to each protocol should also be set + // TODO: scheme could be 'custom' in our case and we might have to ask 'protocol' separately to the user + String protocol = details.get(Constants.PROTOCOL); + switch (protocol.toLowerCase()) { + case Constants.NFS: + parameters.setType(Storage.StoragePoolType.NetworkFilesystem); + break; + case Constants.ISCSI: + parameters.setType(Storage.StoragePoolType.Iscsi); + break; + default: + throw new CloudRuntimeException("Unsupported protocol: " + scheme + ", cannot create primary storage"); + } - return null; + details.put(Constants.MANAGEMENTLIF, url); + // Validate the ONTAP details + if(details.get(Constants.ISDISAGGREGATED) == null || details.get(Constants.ISDISAGGREGATED).isEmpty()) { + details.put(Constants.ISDISAGGREGATED, "false"); + } + + OntapStorage ontapStorage = new OntapStorage(details.get(Constants.USERNAME), details.get(Constants.PASSWORD), + details.get(Constants.MANAGEMENTLIF), details.get(Constants.SVMNAME), details.get(Constants.PROTOCOL), + Boolean.parseBoolean(details.get(Constants.ISDISAGGREGATED))); + StorageProviderFactory storageProviderManager = new StorageProviderFactory(ontapStorage); + StorageStrategy storageStrategy = storageProviderManager.getStrategy(); + boolean isValid = storageStrategy.connect(); + if (isValid) { +// String volumeName = storagePoolName + "_vol"; //TODO: Figure out a better naming convention + storageStrategy.createVolume(storagePoolName, Long.parseLong((details.get("size")))); // TODO: size should be in bytes, so see if conversion is needed + } else { + throw new CloudRuntimeException("ONTAP details validation failed, cannot create primary storage"); + } + + parameters.setTags(tags); + parameters.setIsTagARule(isTagARule); + parameters.setDetails(details); + parameters.setUuid(UUID.randomUUID().toString()); + parameters.setZoneId(zoneId); + parameters.setPodId(podId); + parameters.setClusterId(clusterId); + parameters.setName(storagePoolName); + parameters.setProviderName(providerName); + parameters.setManaged(true); + + return _dataStoreHelper.createPrimaryDataStore(parameters); } @Override - public boolean attachCluster(DataStore store, ClusterScope scope) { - return false; + public boolean attachCluster(DataStore dataStore, ClusterScope scope) { + logger.debug("In attachCluster for ONTAP primary storage"); + PrimaryDataStoreInfo primarystore = (PrimaryDataStoreInfo)dataStore; + List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInClusterForStorageConnection(primarystore); + + logger.debug(String.format("Attaching the pool to each of the hosts %s in the cluster: %s", hostsToConnect, primarystore.getClusterId())); + for (HostVO host : hostsToConnect) { + // TODO: Fetch the host IQN and add to the initiator group on ONTAP cluster + try { + _storageMgr.connectHostToSharedPool(host, dataStore.getId()); + } catch (Exception e) { + logger.warn("Unable to establish a connection between " + host + " and " + dataStore, e); + } + } + _dataStoreHelper.attachCluster(dataStore); + return true; } @Override @@ -60,7 +194,20 @@ public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo exis @Override public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) { - return false; + logger.debug("In attachZone for ONTAP primary storage"); + List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore, scope.getScopeId(), Hypervisor.HypervisorType.KVM); + + logger.debug(String.format("In createPool. Attaching the pool to each of the hosts in %s.", hostsToConnect)); + for (HostVO host : hostsToConnect) { + // TODO: Fetch the host IQN and add to the initiator group on ONTAP cluster + try { + _storageMgr.connectHostToSharedPool(host, dataStore.getId()); + } catch (Exception e) { + logger.warn("Unable to establish a connection between " + host + " and " + dataStore, e); + } + } + _dataStoreHelper.attachZone(dataStore); + return true; } @Override diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java new file mode 100644 index 000000000000..263d103e4c42 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java @@ -0,0 +1,64 @@ +/* + * 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.provider; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.storage.feign.model.OntapStorage; +import org.apache.cloudstack.storage.service.StorageStrategy; +import org.apache.cloudstack.storage.service.UnifiedNASStrategy; +import org.apache.cloudstack.storage.service.UnifiedSANStrategy; +import org.apache.cloudstack.storage.utils.Constants; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +@Component +public class StorageProviderFactory { + private final StorageStrategy storageStrategy; + private static final Logger s_logger = (Logger) LogManager.getLogger(StorageProviderFactory.class); + + public StorageProviderFactory(OntapStorage ontapStorage) { + String protocol = ontapStorage.getProtocol(); + s_logger.info("Initializing StorageProviderFactory with protocol: " + protocol); + switch (protocol.toLowerCase()) { + case Constants.NFS: + if(!ontapStorage.getIsDisaggregated()) { + this.storageStrategy = new UnifiedNASStrategy(ontapStorage); + } else { + throw new CloudRuntimeException("Unsupported configuration: Disaggregated ONTAP is not supported."); + } + break; + case Constants.ISCSI: + if (!ontapStorage.getIsDisaggregated()) { + this.storageStrategy = new UnifiedSANStrategy(ontapStorage); + } else { + throw new CloudRuntimeException("Unsupported configuration: Disaggregated ONTAP is not supported."); + } + break; + default: + this.storageStrategy = null; + throw new CloudRuntimeException("Unsupported protocol: " + protocol); + } + } + + public StorageStrategy getStrategy() { + return storageStrategy; + } +} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/NASStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/NASStrategy.java new file mode 100644 index 000000000000..4e03daae4b4a --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/NASStrategy.java @@ -0,0 +1,34 @@ +/* + * 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.service; + +import org.apache.cloudstack.storage.feign.model.OntapStorage; + +public abstract class NASStrategy extends StorageStrategy { + public NASStrategy(OntapStorage ontapStorage) { + super(ontapStorage); + } + + public abstract String createExportPolicy(String svmName, String policyName); + public abstract String addExportRule(String policyName, String clientMatch, String[] protocols, String[] roRule, String[] rwRule); + public abstract String assignExportPolicyToVolume(String volumeUuid, String policyName); + public abstract String enableNFS(String svmUuid); +} + diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java new file mode 100644 index 000000000000..4e6846ef7610 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java @@ -0,0 +1,33 @@ +/* + * 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.service; + +import org.apache.cloudstack.storage.feign.model.OntapStorage; + +public abstract class SANStrategy extends StorageStrategy { + public SANStrategy(OntapStorage ontapStorage) { + super(ontapStorage); + } + + public abstract String createLUN(String svmName, String volumeName, String lunName, long sizeBytes, String osType); + public abstract String createIgroup(String svmName, String igroupName, String[] initiators); + public abstract String mapLUNToIgroup(String lunName, String igroupName); + public abstract String enableISCSI(String svmUuid); +} 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 new file mode 100644 index 000000000000..8799a25d1569 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java @@ -0,0 +1,173 @@ +/* + * 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.service; + +import com.cloud.utils.exception.CloudRuntimeException; +import feign.FeignException; +import org.apache.cloudstack.storage.feign.client.JobFeignClient; +import org.apache.cloudstack.storage.feign.client.SvmFeignClient; +import org.apache.cloudstack.storage.feign.client.VolumeFeignClient; +import org.apache.cloudstack.storage.feign.model.Aggregate; +import org.apache.cloudstack.storage.feign.model.Job; +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.response.JobResponse; +import org.apache.cloudstack.storage.feign.model.response.OntapResponse; +import org.apache.cloudstack.storage.utils.Constants; +import org.apache.cloudstack.storage.utils.Utility; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.inject.Inject; +import java.net.URI; +import java.util.List; +import java.util.Objects; + +public abstract class StorageStrategy { + @Inject + private Utility utils; + + @Inject + private VolumeFeignClient volumeFeignClient; + + @Inject + private SvmFeignClient svmFeignClient; + + @Inject + private JobFeignClient jobFeignClient; + + private final OntapStorage storage; + + private List aggregates; + + private static final Logger s_logger = (Logger) LogManager.getLogger(StorageStrategy.class); + + public StorageStrategy(OntapStorage ontapStorage) { + storage = ontapStorage; + } + + // Connect method to validate ONTAP cluster, credentials, protocol, and SVM + public boolean connect() { + s_logger.info("Attempting to connect to ONTAP cluster at " + storage.getManagementLIF()); + //Get AuthHeader + String authHeader = utils.generateAuthHeader(storage.getUsername(), storage.getPassword()); + try { + // Call the SVM API to check if the SVM exists + Svm svm = null; + URI url = URI.create(Constants.HTTPS + storage.getManagementLIF() + Constants.GETSVMs); + OntapResponse svms = svmFeignClient.getSvms(url, authHeader); + for (Svm storageVM : svms.getRecords()) { + if (storageVM.getName().equals(storage.getSVM())) { + svm = storageVM; + s_logger.info("Found SVM: " + storage.getSVM()); + break; + } + } + + // Validations + if (svm == null) { + s_logger.error("SVM with name " + storage.getSVM() + " not found."); + throw new CloudRuntimeException("SVM with name " + storage.getSVM() + " not found."); + } else { + if (svm.getState() != Constants.RUNNING) { + s_logger.error("SVM " + storage.getSVM() + " is not in running state."); + throw new CloudRuntimeException("SVM " + storage.getSVM() + " is not in running state."); + } + if (Objects.equals(storage.getProtocol(), Constants.NFS) && !svm.getNfsEnabled()) { + s_logger.error("NFS protocol is not enabled on SVM " + storage.getSVM()); + throw new CloudRuntimeException("NFS protocol is not enabled on SVM " + storage.getSVM()); + } else if (Objects.equals(storage.getProtocol(), Constants.ISCSI) && !svm.getIscsiEnabled()) { + s_logger.error("iSCSI protocol is not enabled on SVM " + storage.getSVM()); + throw new CloudRuntimeException("iSCSI protocol is not enabled on SVM " + storage.getSVM()); + } + List aggrs = svm.getAggregates(); + if (aggrs == null || aggrs.isEmpty()) { + s_logger.error("No aggregates are assigned to SVM " + storage.getSVM()); + throw new CloudRuntimeException("No aggregates are assigned to SVM " + storage.getSVM()); + } + this.aggregates = aggrs; + } + s_logger.info("Successfully connected to ONTAP cluster and validated ONTAP details provided"); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to connect to ONTAP cluster: " + e.getMessage()); + } + return true; + } + + // Common methods like create/delete etc., should be here + public void createVolume(String volumeName, Long size) { + s_logger.info("Creating volume: " + volumeName + " of size: " + size + " bytes"); + + if (aggregates == null || aggregates.isEmpty()) { + s_logger.error("No aggregates available to create volume on SVM " + storage.getSVM()); + throw new CloudRuntimeException("No aggregates available to create volume on SVM " + storage.getSVM()); + } + // Get the AuthHeader + String authHeader = utils.generateAuthHeader(storage.getUsername(), storage.getPassword()); + + // Generate the Create Volume Request + Volume volumeRequest = new Volume(); + Svm svm = new Svm(); + svm.setName(storage.getSVM()); + + volumeRequest.setName(volumeName); + volumeRequest.setSvm(svm); + volumeRequest.setAggregates(aggregates); + volumeRequest.setSize(size); + // Make the POST API call to create the volume + try { + // Create URI for POST CreateVolume API + URI url = utils.generateURI(Constants.CREATEVOLUME); + // Call the VolumeFeignClient to create the volume + JobResponse jobResponse = volumeFeignClient.createVolumeWithJob(url, authHeader, volumeRequest); + String jobUUID = jobResponse.getJob().getUuid(); + + //Create URI for GET Job API + url = utils.generateURI(Constants.GETJOBBYUUID); + int jobRetryCount = 0, maxJobRetries = Constants.JOBMAXRETRIES; + Job createVolumeJob = null; + while(createVolumeJob == null || createVolumeJob.getState().equals(Constants.JOBRUNNING) || createVolumeJob.getState().equals(Constants.JOBQUEUE) || createVolumeJob.getState().equals(Constants.JOBPAUSED)) { + if(jobRetryCount >= maxJobRetries) { + s_logger.error("Job to create volume " + volumeName + " did not complete within expected time."); + throw new CloudRuntimeException("Job to create volume " + volumeName + " did not complete within expected time."); + } + + try { + createVolumeJob = jobFeignClient.getJobByUUID(url, authHeader, jobUUID); + if (createVolumeJob == null) { + s_logger.warn("Job with UUID " + jobUUID + " not found. Retrying..."); + } else if (createVolumeJob.getState().equals(Constants.JOBFAILURE)) { + throw new CloudRuntimeException("Job to create volume " + volumeName + " failed with error: " + createVolumeJob.getMessage()); + } + } catch (FeignException.FeignClientException e) { + throw new CloudRuntimeException("Failed to fetch job status: " + e.getMessage()); + } + + jobRetryCount++; + Thread.sleep(Constants.CREATEVOLUMECHECKSLEEPTIME); // Sleep for 2 seconds before polling again + } + } catch (Exception e) { + s_logger.error("Exception while creating volume: ", e); + throw new CloudRuntimeException("Failed to create volume: " + e.getMessage()); + } + s_logger.info("Volume created successfully: " + volumeName); + } +} 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 new file mode 100644 index 000000000000..6c9a8735c4c1 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java @@ -0,0 +1,48 @@ +/* + * 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.service; + +import org.apache.cloudstack.storage.feign.model.OntapStorage; + +public class UnifiedNASStrategy extends NASStrategy{ + public UnifiedNASStrategy(OntapStorage ontapStorage) { + super(ontapStorage); + } + + @Override + public String createExportPolicy(String svmName, String policyName) { + return ""; + } + + @Override + public String addExportRule(String policyName, String clientMatch, String[] protocols, String[] roRule, String[] rwRule) { + return ""; + } + + @Override + public String assignExportPolicyToVolume(String volumeUuid, String policyName) { + return ""; + } + + @Override + public String enableNFS(String svmUuid) { + return ""; + } +} 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 new file mode 100644 index 000000000000..e954ec312006 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java @@ -0,0 +1,48 @@ +/* + * 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.service; + +import org.apache.cloudstack.storage.feign.model.OntapStorage; + +public class UnifiedSANStrategy extends SANStrategy{ + public UnifiedSANStrategy(OntapStorage ontapStorage) { + super(ontapStorage); + } + + @Override + public String createLUN(String svmName, String volumeName, String lunName, long sizeBytes, String osType) { + return ""; + } + + @Override + public String createIgroup(String svmName, String igroupName, String[] initiators) { + return ""; + } + + @Override + public String mapLUNToIgroup(String lunName, String igroupName) { + return ""; + } + + @Override + public String enableISCSI(String svmUuid) { + return ""; + } +} 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 new file mode 100644 index 000000000000..0dff67941e5e --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java @@ -0,0 +1,46 @@ +/* + * 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.utils; + +public class Constants { + public static final String NFS = "nfs"; + public static final String ISCSI = "iscsi"; + public static final String PROTOCOL = "protocol"; + public static final String SVMNAME = "svmName"; + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String MANAGEMENTLIF = "managementLIF"; + public static final String ISDISAGGREGATED = "isDisaggregated"; + public static final String RUNNING = "running"; + + public static final String JOBRUNNING = "running"; + public static final String JOBQUEUE = "queued"; + public static final String JOBPAUSED = "paused"; + public static final String JOBFAILURE = "failure"; + public static final String JOBSUCCESS = "success"; + + public static final int JOBMAXRETRIES = 100; + public static final int CREATEVOLUMECHECKSLEEPTIME = 2000; + + public static final String HTTPS = "https://"; + public static final String GETSVMs = "/api/svm/svms"; + public static final String CREATEVOLUME = "/api/storage/volumes"; + public static final String GETJOBBYUUID = "/api/cluster/jobs"; +} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java new file mode 100644 index 000000000000..6fcf155e27b5 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java @@ -0,0 +1,52 @@ +/* + * 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.utils; + +import com.cloud.utils.StringUtils; +import org.apache.cloudstack.storage.feign.model.OntapStorage; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +import javax.inject.Inject; +import java.net.URI; + +@Component +public class Utility { + @Inject + OntapStorage ontapStorage; + + private static final String BASIC = "Basic"; + private static final String AUTH_HEADER_COLON = ":"; + /** + * Method generates authentication headers using storage backend credentials passed as normal string + * @param username -->> username of the storage backend + * @param password -->> normal decoded password of the storage backend + * @return + */ + public String generateAuthHeader(String username, String password) { + byte[] encodedBytes = Base64Utils.encode((username + AUTH_HEADER_COLON + password).getBytes()); + return BASIC + StringUtils.SPACE + new String(encodedBytes); + } + + public URI generateURI(String path) { + String uriString = Constants.HTTPS + ontapStorage.getManagementLIF() + path; + return URI.create(uriString); + } +} From a9c7f651fccedbb6758822b8d4775eb25d208fac Mon Sep 17 00:00:00 2001 From: "Locharla, Sandeep" Date: Wed, 22 Oct 2025 20:02:48 +0530 Subject: [PATCH 2/4] CSTACKEX-7: Addressed review comments --- .../storage/feign/client/SvmFeignClient.java | 4 +- .../storage/feign/model/OntapStorage.java | 22 +++--- .../feign/model/response/OntapResponse.java | 6 +- .../OntapPrimaryDatastoreLifecycle.java | 39 +++++----- .../storage/service/StorageStrategy.java | 78 +++++++++---------- .../cloudstack/storage/utils/Constants.java | 31 ++++---- 6 files changed, 93 insertions(+), 87 deletions(-) diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java index 57c1cfb6b3ed..f34cf4289c7b 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java @@ -26,15 +26,17 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import java.net.URI; +import java.util.Map; @FeignClient(name = "SvmClient", url = "https://{clusterIP}/api/svm/svms", configuration = FeignConfiguration.class) public interface SvmFeignClient { //this method to get all svms and also filtered svms based on query params as a part of URL @RequestMapping(method = RequestMethod.GET) - OntapResponse getSvms(URI baseURL, @RequestHeader("Authorization") String header); + OntapResponse getSvmResponse(URI baseURL, @RequestHeader("Authorization") String header, @RequestParam Map queryParams); @RequestMapping(method = RequestMethod.GET, value = "/{uuid}") Svm getSvmByUUID(URI baseURL, @RequestHeader("Authorization") String header); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java index d02112422f60..776149c9697e 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java @@ -19,19 +19,21 @@ package org.apache.cloudstack.storage.feign.model; +import org.apache.cloudstack.storage.utils.Constants.ProtocolType; + public class OntapStorage { public static String Username; public static String Password; public static String ManagementLIF; - public static String Svm; - public static String Protocol; + public static String SvmName; + public static ProtocolType Protocol; public static Boolean IsDisaggregated; - public OntapStorage(String username, String password, String managementLIF, String svm, String protocol, Boolean isDisaggregated) { + public OntapStorage(String username, String password, String managementLIF, String svmName, ProtocolType protocol, Boolean isDisaggregated) { Username = username; Password = password; ManagementLIF = managementLIF; - Svm = svm; + SvmName = svmName; Protocol = protocol; IsDisaggregated = isDisaggregated; } @@ -60,19 +62,19 @@ public void setManagementLIF(String managementLIF) { ManagementLIF = managementLIF; } - public String getSVM() { - return Svm; + public String getSvmName() { + return SvmName; } - public void setSVM(String svm) { - Svm = svm; + public void setSvmName(String svmName) { + SvmName = svmName; } - public String getProtocol() { + public ProtocolType getProtocol() { return Protocol; } - public void setProtocol(String protocol) { + public void setProtocol(ProtocolType protocol) { Protocol = protocol; } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java index b78f41e7df3b..87af68fe378e 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/response/OntapResponse.java @@ -34,11 +34,11 @@ public class OntapResponse { @JsonProperty("records") private List records; - public OntapResponse() { + public OntapResponse () { // Default constructor } - public OntapResponse(List records) { + public OntapResponse (List records) { this.records = records; this.numRecords = (records != null) ? records.size() : 0; } @@ -59,4 +59,4 @@ public void setRecords(List records) { this.records = records; this.numRecords = (records != null) ? records.size() : 0; } -} +} \ No newline at end of file diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java index cc7eb5618f1a..c55242243779 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java @@ -43,6 +43,7 @@ import org.apache.cloudstack.storage.provider.StorageProviderFactory; import org.apache.cloudstack.storage.service.StorageStrategy; import org.apache.cloudstack.storage.utils.Constants; +import org.apache.cloudstack.storage.utils.Constants.ProtocolType; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -70,12 +71,12 @@ public DataStore initialize(Map dsInfos) { throw new CloudRuntimeException("Datastore info map is null, cannot create primary storage"); } String url = dsInfos.get("url").toString(); // TODO: Decide on whether should the customer enter just the Management LIF IP or https://ManagementLIF - Long zoneId = (Long) dsInfos.get("zoneId"); - Long podId = (Long)dsInfos.get("podId"); - Long clusterId = (Long)dsInfos.get("clusterId"); - String storagePoolName = dsInfos.get("name").toString(); - String providerName = dsInfos.get("providerName").toString(); - String tags = dsInfos.get("tags").toString(); + Long zoneId = dsInfos.get("zoneId").toString().trim().isEmpty() ? null : (Long)dsInfos.get("zoneId"); + Long podId = dsInfos.get("podId").toString().trim().isEmpty() ? null : (Long)dsInfos.get("zoneId"); + Long clusterId = dsInfos.get("clusterId").toString().trim().isEmpty() ? null : (Long)dsInfos.get("clusterId"); + String storagePoolName = dsInfos.get("name").toString().trim(); + String providerName = dsInfos.get("providerName").toString().trim(); + String tags = dsInfos.get("tags").toString().trim(); Boolean isTagARule = (Boolean) dsInfos.get("isTagARule"); String scheme = dsInfos.get("scheme").toString(); @@ -86,12 +87,8 @@ public DataStore initialize(Map dsInfos) { @SuppressWarnings("unchecked") Map details = (Map)dsInfos.get("details"); // Validations - if (podId != null && clusterId == null) { - s_logger.error("Cluster Id is null, cannot create primary storage"); - return null; - } else if (podId == null && clusterId != null) { - s_logger.error("Pod Id is null, cannot create primary storage"); - return null; + if (podId == null ^ clusterId == null) { + throw new CloudRuntimeException("Cluster Id or Pod Id is null, cannot create primary storage"); } if (podId == null && clusterId == null) { @@ -122,28 +119,28 @@ public DataStore initialize(Map dsInfos) { // TODO: While testing need to check what does this actually do and if the fields corresponding to each protocol should also be set // TODO: scheme could be 'custom' in our case and we might have to ask 'protocol' separately to the user - String protocol = details.get(Constants.PROTOCOL); - switch (protocol.toLowerCase()) { - case Constants.NFS: + ProtocolType protocol = ProtocolType.valueOf(details.get(Constants.PROTOCOL).toLowerCase()); + switch (protocol) { + case NFS: parameters.setType(Storage.StoragePoolType.NetworkFilesystem); break; - case Constants.ISCSI: + case ISCSI: parameters.setType(Storage.StoragePoolType.Iscsi); break; default: throw new CloudRuntimeException("Unsupported protocol: " + scheme + ", cannot create primary storage"); } - details.put(Constants.MANAGEMENTLIF, url); + details.put(Constants.MANAGEMENT_LIF, url); // Validate the ONTAP details - if(details.get(Constants.ISDISAGGREGATED) == null || details.get(Constants.ISDISAGGREGATED).isEmpty()) { - details.put(Constants.ISDISAGGREGATED, "false"); + if(details.get(Constants.IS_DISAGGREGATED) == null || details.get(Constants.IS_DISAGGREGATED).isEmpty()) { + details.put(Constants.IS_DISAGGREGATED, "false"); } OntapStorage ontapStorage = new OntapStorage(details.get(Constants.USERNAME), details.get(Constants.PASSWORD), - details.get(Constants.MANAGEMENTLIF), details.get(Constants.SVMNAME), details.get(Constants.PROTOCOL), - Boolean.parseBoolean(details.get(Constants.ISDISAGGREGATED))); + details.get(Constants.MANAGEMENT_LIF), details.get(Constants.SVM_NAME), protocol, + Boolean.parseBoolean(details.get(Constants.IS_DISAGGREGATED))); StorageProviderFactory storageProviderManager = new StorageProviderFactory(ontapStorage); StorageStrategy storageStrategy = storageProviderManager.getStrategy(); boolean isValid = storageStrategy.connect(); 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 8799a25d1569..47365dfd584c 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 @@ -37,6 +37,7 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.util.Map; import java.net.URI; import java.util.List; import java.util.Objects; @@ -69,42 +70,37 @@ public boolean connect() { s_logger.info("Attempting to connect to ONTAP cluster at " + storage.getManagementLIF()); //Get AuthHeader String authHeader = utils.generateAuthHeader(storage.getUsername(), storage.getPassword()); + String svmName = storage.getSvmName(); try { // Call the SVM API to check if the SVM exists Svm svm = null; - URI url = URI.create(Constants.HTTPS + storage.getManagementLIF() + Constants.GETSVMs); - OntapResponse svms = svmFeignClient.getSvms(url, authHeader); - for (Svm storageVM : svms.getRecords()) { - if (storageVM.getName().equals(storage.getSVM())) { - svm = storageVM; - s_logger.info("Found SVM: " + storage.getSVM()); - break; - } + URI url = URI.create(Constants.HTTPS + storage.getManagementLIF() + Constants.GET_SVMs); + Map queryParams = Map.of("name", svmName); + OntapResponse svms = svmFeignClient.getSvmResponse(url, authHeader, queryParams); + if (svms != null && svms.getRecords() != null && !svms.getRecords().isEmpty()) { + svm = svms.getRecords().get(0); + } else { + throw new CloudRuntimeException("No SVM found on the ONTAP cluster by the name" + svmName + "."); } // Validations - if (svm == null) { - s_logger.error("SVM with name " + storage.getSVM() + " not found."); - throw new CloudRuntimeException("SVM with name " + storage.getSVM() + " not found."); - } else { - if (svm.getState() != Constants.RUNNING) { - s_logger.error("SVM " + storage.getSVM() + " is not in running state."); - throw new CloudRuntimeException("SVM " + storage.getSVM() + " is not in running state."); - } - if (Objects.equals(storage.getProtocol(), Constants.NFS) && !svm.getNfsEnabled()) { - s_logger.error("NFS protocol is not enabled on SVM " + storage.getSVM()); - throw new CloudRuntimeException("NFS protocol is not enabled on SVM " + storage.getSVM()); - } else if (Objects.equals(storage.getProtocol(), Constants.ISCSI) && !svm.getIscsiEnabled()) { - s_logger.error("iSCSI protocol is not enabled on SVM " + storage.getSVM()); - throw new CloudRuntimeException("iSCSI protocol is not enabled on SVM " + storage.getSVM()); - } - List aggrs = svm.getAggregates(); - if (aggrs == null || aggrs.isEmpty()) { - s_logger.error("No aggregates are assigned to SVM " + storage.getSVM()); - throw new CloudRuntimeException("No aggregates are assigned to SVM " + storage.getSVM()); - } - this.aggregates = aggrs; + if (!Objects.equals(svm.getState(), Constants.RUNNING)) { + s_logger.error("SVM " + svmName + " is not in running state."); + throw new CloudRuntimeException("SVM " + svmName + " is not in running state."); + } + if (Objects.equals(storage.getProtocol(), Constants.NFS) && !svm.getNfsEnabled()) { + s_logger.error("NFS protocol is not enabled on SVM " + svmName); + throw new CloudRuntimeException("NFS protocol is not enabled on SVM " + svmName); + } else if (Objects.equals(storage.getProtocol(), Constants.ISCSI) && !svm.getIscsiEnabled()) { + s_logger.error("iSCSI protocol is not enabled on SVM " + svmName); + throw new CloudRuntimeException("iSCSI protocol is not enabled on SVM " + svmName); } + List aggrs = svm.getAggregates(); + if (aggrs == null || aggrs.isEmpty()) { + s_logger.error("No aggregates are assigned to SVM " + svmName); + throw new CloudRuntimeException("No aggregates are assigned to SVM " + svmName); + } + this.aggregates = aggrs; s_logger.info("Successfully connected to ONTAP cluster and validated ONTAP details provided"); } catch (Exception e) { throw new CloudRuntimeException("Failed to connect to ONTAP cluster: " + e.getMessage()); @@ -116,9 +112,10 @@ public boolean connect() { public void createVolume(String volumeName, Long size) { s_logger.info("Creating volume: " + volumeName + " of size: " + size + " bytes"); + String svmName = storage.getSvmName(); if (aggregates == null || aggregates.isEmpty()) { - s_logger.error("No aggregates available to create volume on SVM " + storage.getSVM()); - throw new CloudRuntimeException("No aggregates available to create volume on SVM " + storage.getSVM()); + s_logger.error("No aggregates available to create volume on SVM " + svmName); + throw new CloudRuntimeException("No aggregates available to create volume on SVM " + svmName); } // Get the AuthHeader String authHeader = utils.generateAuthHeader(storage.getUsername(), storage.getPassword()); @@ -126,7 +123,7 @@ public void createVolume(String volumeName, Long size) { // Generate the Create Volume Request Volume volumeRequest = new Volume(); Svm svm = new Svm(); - svm.setName(storage.getSVM()); + svm.setName(svmName); volumeRequest.setName(volumeName); volumeRequest.setSvm(svm); @@ -135,17 +132,20 @@ public void createVolume(String volumeName, Long size) { // Make the POST API call to create the volume try { // Create URI for POST CreateVolume API - URI url = utils.generateURI(Constants.CREATEVOLUME); + URI url = utils.generateURI(Constants.CREATE_VOLUME); // Call the VolumeFeignClient to create the volume JobResponse jobResponse = volumeFeignClient.createVolumeWithJob(url, authHeader, volumeRequest); + if (jobResponse == null || jobResponse.getJob() == null) { + throw new CloudRuntimeException("Failed to initiate volume creation for " + volumeName); + } String jobUUID = jobResponse.getJob().getUuid(); //Create URI for GET Job API - url = utils.generateURI(Constants.GETJOBBYUUID); - int jobRetryCount = 0, maxJobRetries = Constants.JOBMAXRETRIES; + url = utils.generateURI(Constants.GET_JOB_BY_UUID); + int jobRetryCount = 0; Job createVolumeJob = null; - while(createVolumeJob == null || createVolumeJob.getState().equals(Constants.JOBRUNNING) || createVolumeJob.getState().equals(Constants.JOBQUEUE) || createVolumeJob.getState().equals(Constants.JOBPAUSED)) { - if(jobRetryCount >= maxJobRetries) { + while(createVolumeJob == null || !createVolumeJob.getState().equals(Constants.JOB_SUCCESS)) { + if(jobRetryCount >= Constants.JOB_MAX_RETRIES) { s_logger.error("Job to create volume " + volumeName + " did not complete within expected time."); throw new CloudRuntimeException("Job to create volume " + volumeName + " did not complete within expected time."); } @@ -154,7 +154,7 @@ public void createVolume(String volumeName, Long size) { createVolumeJob = jobFeignClient.getJobByUUID(url, authHeader, jobUUID); if (createVolumeJob == null) { s_logger.warn("Job with UUID " + jobUUID + " not found. Retrying..."); - } else if (createVolumeJob.getState().equals(Constants.JOBFAILURE)) { + } else if (createVolumeJob.getState().equals(Constants.JOB_FAILURE)) { throw new CloudRuntimeException("Job to create volume " + volumeName + " failed with error: " + createVolumeJob.getMessage()); } } catch (FeignException.FeignClientException e) { @@ -162,7 +162,7 @@ public void createVolume(String volumeName, Long size) { } jobRetryCount++; - Thread.sleep(Constants.CREATEVOLUMECHECKSLEEPTIME); // Sleep for 2 seconds before polling again + Thread.sleep(Constants.CREATE_VOLUME_CHECK_SLEEP_TIME); // Sleep for 2 seconds before polling again } } catch (Exception e) { s_logger.error("Exception while creating volume: ", e); 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 0dff67941e5e..4fe55eb2e1fd 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 @@ -20,27 +20,32 @@ package org.apache.cloudstack.storage.utils; public class Constants { + public enum ProtocolType { + NFS, + ISCSI + } + public static final String NFS = "nfs"; public static final String ISCSI = "iscsi"; public static final String PROTOCOL = "protocol"; - public static final String SVMNAME = "svmName"; + public static final String SVM_NAME = "svmName"; public static final String USERNAME = "username"; public static final String PASSWORD = "password"; - public static final String MANAGEMENTLIF = "managementLIF"; - public static final String ISDISAGGREGATED = "isDisaggregated"; + public static final String MANAGEMENT_LIF = "managementLIF"; + public static final String IS_DISAGGREGATED = "isDisaggregated"; public static final String RUNNING = "running"; - public static final String JOBRUNNING = "running"; - public static final String JOBQUEUE = "queued"; - public static final String JOBPAUSED = "paused"; - public static final String JOBFAILURE = "failure"; - public static final String JOBSUCCESS = "success"; + public static final String JOB_RUNNING = "running"; + public static final String JOB_QUEUE = "queued"; + public static final String JOB_PAUSED = "paused"; + public static final String JOB_FAILURE = "failure"; + public static final String JOB_SUCCESS = "success"; - public static final int JOBMAXRETRIES = 100; - public static final int CREATEVOLUMECHECKSLEEPTIME = 2000; + public static final int JOB_MAX_RETRIES = 100; + public static final int CREATE_VOLUME_CHECK_SLEEP_TIME = 2000; public static final String HTTPS = "https://"; - public static final String GETSVMs = "/api/svm/svms"; - public static final String CREATEVOLUME = "/api/storage/volumes"; - public static final String GETJOBBYUUID = "/api/cluster/jobs"; + public static final String GET_SVMs = "/api/svm/svms"; + public static final String CREATE_VOLUME = "/api/storage/volumes"; + public static final String GET_JOB_BY_UUID = "/api/cluster/jobs"; } From 24d3275cace0880794c68aa70165d88e1c55c83f Mon Sep 17 00:00:00 2001 From: "Locharla, Sandeep" Date: Thu, 23 Oct 2025 13:50:03 +0530 Subject: [PATCH 3/4] CSTACKEX-7: Addressed few more comments --- .../lifecycle/OntapPrimaryDatastoreLifecycle.java | 5 ++--- .../storage/provider/StorageProviderFactory.java | 15 ++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java index c55242243779..722ed1e8c707 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java @@ -112,7 +112,7 @@ public DataStore initialize(Map dsInfos) { ClusterVO clusterVO = _clusterDao.findById(clusterId); Preconditions.checkNotNull(clusterVO, "Unable to locate the specified cluster"); if (clusterVO.getHypervisorType() != Hypervisor.HypervisorType.KVM) { - throw new CloudRuntimeException("ONTAP primary storage is not supported for KVM hypervisor"); + throw new CloudRuntimeException("ONTAP primary storage is supported only for KVM hypervisor"); } parameters.setHypervisorType(clusterVO.getHypervisorType()); } @@ -141,8 +141,7 @@ public DataStore initialize(Map dsInfos) { OntapStorage ontapStorage = new OntapStorage(details.get(Constants.USERNAME), details.get(Constants.PASSWORD), details.get(Constants.MANAGEMENT_LIF), details.get(Constants.SVM_NAME), protocol, Boolean.parseBoolean(details.get(Constants.IS_DISAGGREGATED))); - StorageProviderFactory storageProviderManager = new StorageProviderFactory(ontapStorage); - StorageStrategy storageStrategy = storageProviderManager.getStrategy(); + StorageStrategy storageStrategy = StorageProviderFactory.getStrategy(ontapStorage); boolean isValid = storageStrategy.connect(); if (isValid) { // String volumeName = storagePoolName + "_vol"; //TODO: Figure out a better naming convention diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java index 263d103e4c42..1bc6f51798ba 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java @@ -25,6 +25,7 @@ import org.apache.cloudstack.storage.service.UnifiedNASStrategy; import org.apache.cloudstack.storage.service.UnifiedSANStrategy; import org.apache.cloudstack.storage.utils.Constants; +import org.apache.cloudstack.storage.utils.Constants.ProtocolType; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @@ -34,18 +35,18 @@ public class StorageProviderFactory { private final StorageStrategy storageStrategy; private static final Logger s_logger = (Logger) LogManager.getLogger(StorageProviderFactory.class); - public StorageProviderFactory(OntapStorage ontapStorage) { - String protocol = ontapStorage.getProtocol(); + private StorageProviderFactory(OntapStorage ontapStorage) { + ProtocolType protocol = ontapStorage.getProtocol(); s_logger.info("Initializing StorageProviderFactory with protocol: " + protocol); - switch (protocol.toLowerCase()) { - case Constants.NFS: + switch (protocol) { + case NFS: if(!ontapStorage.getIsDisaggregated()) { this.storageStrategy = new UnifiedNASStrategy(ontapStorage); } else { throw new CloudRuntimeException("Unsupported configuration: Disaggregated ONTAP is not supported."); } break; - case Constants.ISCSI: + case ISCSI: if (!ontapStorage.getIsDisaggregated()) { this.storageStrategy = new UnifiedSANStrategy(ontapStorage); } else { @@ -58,7 +59,7 @@ public StorageProviderFactory(OntapStorage ontapStorage) { } } - public StorageStrategy getStrategy() { - return storageStrategy; + public static StorageStrategy getStrategy(OntapStorage ontapStorage) { + return new StorageProviderFactory(ontapStorage).storageStrategy; } } From 2274e607e7d05039592de420f519692bf8c34e1a Mon Sep 17 00:00:00 2001 From: "Locharla, Sandeep" Date: Fri, 24 Oct 2025 08:25:00 +0530 Subject: [PATCH 4/4] CSTACKEX-7: Made changes to correct an exception message and some variable names --- .../storage/feign/client/SvmFeignClient.java | 2 +- .../storage/feign/model/OntapStorage.java | 54 +++++++++---------- .../storage/service/StorageStrategy.java | 7 ++- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java index f34cf4289c7b..753595713c25 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SvmFeignClient.java @@ -36,7 +36,7 @@ public interface SvmFeignClient { //this method to get all svms and also filtered svms based on query params as a part of URL @RequestMapping(method = RequestMethod.GET) - OntapResponse getSvmResponse(URI baseURL, @RequestHeader("Authorization") String header, @RequestParam Map queryParams); + OntapResponse getSvmResponse(URI baseURL, @RequestHeader("Authorization") String header); @RequestMapping(method = RequestMethod.GET, value = "/{uuid}") Svm getSvmByUUID(URI baseURL, @RequestHeader("Authorization") String header); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java index 776149c9697e..af986e5fdc39 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/OntapStorage.java @@ -22,67 +22,67 @@ import org.apache.cloudstack.storage.utils.Constants.ProtocolType; public class OntapStorage { - public static String Username; - public static String Password; - public static String ManagementLIF; - public static String SvmName; - public static ProtocolType Protocol; - public static Boolean IsDisaggregated; - - public OntapStorage(String username, String password, String managementLIF, String svmName, ProtocolType protocol, Boolean isDisaggregated) { - Username = username; - Password = password; - ManagementLIF = managementLIF; - SvmName = svmName; - Protocol = protocol; - IsDisaggregated = isDisaggregated; + public static String _username; + public static String _password; + public static String _managementLIF; + public static String _svmName; + public static ProtocolType _protocolType; + public static Boolean _isDisaggregated; + + public OntapStorage(String username, String password, String managementLIF, String svmName, ProtocolType protocolType, Boolean isDisaggregated) { + _username = username; + _password = password; + _managementLIF = managementLIF; + _svmName = svmName; + _protocolType = protocolType; + _isDisaggregated = isDisaggregated; } public String getUsername() { - return Username; + return _username; } public void setUsername(String username) { - Username = username; + _username = username; } public String getPassword() { - return Password; + return _password; } public void setPassword(String password) { - Password = password; + _password = password; } public String getManagementLIF() { - return ManagementLIF; + return _managementLIF; } public void setManagementLIF(String managementLIF) { - ManagementLIF = managementLIF; + _managementLIF = managementLIF; } public String getSvmName() { - return SvmName; + return _svmName; } public void setSvmName(String svmName) { - SvmName = svmName; + _svmName = svmName; } public ProtocolType getProtocol() { - return Protocol; + return _protocolType; } - public void setProtocol(ProtocolType protocol) { - Protocol = protocol; + public void setProtocol(ProtocolType protocolType) { + _protocolType = protocolType; } public Boolean getIsDisaggregated() { - return IsDisaggregated; + return _isDisaggregated; } public void setIsDisaggregated(Boolean isDisaggregated) { - IsDisaggregated = isDisaggregated; + _isDisaggregated = isDisaggregated; } } 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 47365dfd584c..c608f039b381 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 @@ -73,10 +73,9 @@ public boolean connect() { String svmName = storage.getSvmName(); try { // Call the SVM API to check if the SVM exists - Svm svm = null; - URI url = URI.create(Constants.HTTPS + storage.getManagementLIF() + Constants.GET_SVMs); - Map queryParams = Map.of("name", svmName); - OntapResponse svms = svmFeignClient.getSvmResponse(url, authHeader, queryParams); + Svm svm = new Svm(); + URI url = URI.create(Constants.HTTPS + storage.getManagementLIF() + Constants.GET_SVMs + "?name=" + svmName); + OntapResponse svms = svmFeignClient.getSvmResponse(url, authHeader); if (svms != null && svms.getRecords() != null && !svms.getRecords().isEmpty()) { svm = svms.getRecords().get(0); } else {