Skip to content

Commit bb40df1

Browse files
Merge NetApp main into feature/CSTACKEX-228
Bring in CSTACKEX-158 (idempotent ONTAP snapshot delete) and multi-host NFS/iSCSI support (#64) on top of the apache/main sync.
2 parents dc5b9f7 + 6f949b5 commit bb40df1

10 files changed

Lines changed: 779 additions & 128 deletions

File tree

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private void deleteCloudStackVolumeSnapshot(SnapshotInfo snapshotInfo, CommandRe
316316
commandResult.setSuccess(true);
317317
commandResult.setResult(null);
318318
} catch (Exception e) {
319-
if (isSnapshotNotFoundError(e)) {
319+
if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
320320
logger.warn("deleteCloudStackVolumeSnapshot: ONTAP snapshot for CloudStack snapshot [{}] "
321321
+ "already absent (idempotent success): {}", snapshotId, e.getMessage());
322322
commandResult.setSuccess(true);
@@ -330,25 +330,6 @@ private void deleteCloudStackVolumeSnapshot(SnapshotInfo snapshotInfo, CommandRe
330330
}
331331
}
332332

333-
/**
334-
* Returns true when the exception indicates the ONTAP snapshot was already removed.
335-
* Delete is idempotent: a missing backend snapshot is treated as success.
336-
*/
337-
private boolean isSnapshotNotFoundError(Throwable error) {
338-
if (error == null) {
339-
return false;
340-
}
341-
String message = error.getMessage();
342-
if (message != null) {
343-
String lower = message.toLowerCase();
344-
if (lower.contains("404") || lower.contains("not found") || lower.contains("does not exist")
345-
|| lower.contains("entry doesn't exist")) {
346-
return true;
347-
}
348-
}
349-
return isSnapshotNotFoundError(error.getCause());
350-
}
351-
352333
private long resolveSnapshotPoolId(String poolIdStr, long snapshotId) {
353334
if (poolIdStr != null && !poolIdStr.isEmpty()) {
354335
return Long.parseLong(poolIdStr);

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/ExportRule.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919

2020
package org.apache.cloudstack.storage.feign.model;
2121

22+
import java.util.List;
23+
24+
import com.fasterxml.jackson.annotation.JsonCreator;
2225
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2326
import com.fasterxml.jackson.annotation.JsonInclude;
2427
import com.fasterxml.jackson.annotation.JsonProperty;
25-
import java.util.List;
28+
import com.fasterxml.jackson.annotation.JsonValue;
2629

2730
/**
2831
* ExportRule
@@ -54,6 +57,7 @@ public enum ProtocolsEnum {
5457
this.value = value;
5558
}
5659

60+
@JsonValue
5761
public String getValue() {
5862
return value;
5963
}
@@ -63,9 +67,13 @@ public String toString() {
6367
return String.valueOf(value);
6468
}
6569

70+
@JsonCreator
6671
public static ProtocolsEnum fromValue(String text) {
72+
if (text == null) {
73+
return null;
74+
}
6775
for (ProtocolsEnum b : ProtocolsEnum.values()) {
68-
if (String.valueOf(b.value).equals(text)) {
76+
if (String.valueOf(b.value).equalsIgnoreCase(text)) {
6977
return b;
7078
}
7179
}

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java

Lines changed: 136 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,38 @@
1919

2020
package org.apache.cloudstack.storage.listener;
2121

22+
import java.util.List;
23+
import java.util.Map;
24+
2225
import javax.inject.Inject;
2326

24-
import com.cloud.agent.api.ModifyStoragePoolCommand;
27+
import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
28+
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
29+
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
30+
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
31+
import org.apache.cloudstack.storage.service.StorageStrategy;
32+
import org.apache.cloudstack.storage.service.model.AccessGroup;
33+
import org.apache.cloudstack.storage.service.model.ProtocolType;
34+
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
35+
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
36+
import org.apache.commons.lang3.StringUtils;
37+
import org.apache.logging.log4j.LogManager;
38+
import org.apache.logging.log4j.Logger;
39+
40+
import com.cloud.agent.AgentManager;
41+
import com.cloud.agent.api.Answer;
2542
import com.cloud.agent.api.ModifyStoragePoolAnswer;
43+
import com.cloud.agent.api.ModifyStoragePoolCommand;
2644
import com.cloud.agent.api.StoragePoolInfo;
2745
import com.cloud.alert.AlertManager;
46+
import com.cloud.host.Host;
47+
import com.cloud.host.HostVO;
48+
import com.cloud.host.dao.HostDao;
2849
import com.cloud.hypervisor.Hypervisor;
50+
import com.cloud.storage.StoragePool;
2951
import com.cloud.storage.StoragePoolHostVO;
3052
import com.cloud.storage.dao.StoragePoolHostDao;
31-
import org.apache.logging.log4j.Logger;
32-
import org.apache.logging.log4j.LogManager;
33-
import com.cloud.agent.AgentManager;
34-
import com.cloud.agent.api.Answer;
35-
import com.cloud.agent.api.DeleteStoragePoolCommand;
36-
import com.cloud.host.Host;
37-
import com.cloud.storage.StoragePool;
3853
import com.cloud.utils.exception.CloudRuntimeException;
39-
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
40-
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
41-
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
42-
import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
43-
import com.cloud.host.dao.HostDao;
44-
45-
import java.util.Map;
4654

4755
public class OntapHostListener implements HypervisorHostListener {
4856
protected Logger logger = LogManager.getLogger(getClass());
@@ -63,26 +71,39 @@ public class OntapHostListener implements HypervisorHostListener {
6371

6472
@Override
6573
public boolean hostConnect(long hostId, long poolId) {
66-
logger.info("Connect to host " + hostId + " from pool " + poolId);
74+
logger.info("hostConnect: Connecting host {} to pool {}", hostId, poolId);
6775
Host host = _hostDao.findById(hostId);
6876
if (host == null) {
69-
logger.error("host was not found with id : {}", hostId);
77+
logger.error("hostConnect: Host was not found with id: {}", hostId);
7078
return false;
7179
}
7280
if (!host.getHypervisorType().equals(Hypervisor.HypervisorType.KVM)) {
73-
logger.error("ONTAP plugin does not support {} type host currently ", host.getHypervisorType());
81+
logger.error("hostConnect: ONTAP plugin does not support {} type host currently", host.getHypervisorType());
7482
return false;
7583
}
7684

7785
StoragePool pool = _storagePoolDao.findById(poolId);
7886
if (pool == null) {
79-
logger.error("Failed to connect host - storage pool not found with id: {}", poolId);
87+
logger.error("hostConnect: Failed to connect host - storage pool not found with id: {}", poolId);
8088
return false;
8189
}
82-
logger.info("Connecting host {} to ONTAP storage pool {}", host.getName(), pool.getName());
90+
logger.info("hostConnect: Connecting host {} to ONTAP storage pool {}", host.getName(), pool.getName());
8391
try {
8492
// Load storage pool details from database to pass mount options and other config to agent
8593
Map<String, String> detailsMap = _storagePoolDetailsDao.listDetailsKeyPairs(poolId);
94+
if (detailsMap == null || detailsMap.isEmpty()) {
95+
logger.error("hostConnect: Failed to load storage pool details for pool id: {}", poolId);
96+
return false;
97+
}
98+
99+
if (detailsMap.get(OntapStorageConstants.PROTOCOL) == null) {
100+
logger.error("hostConnect: Storage pool details missing required protocol type for pool id: {}", poolId);
101+
return false;
102+
}
103+
104+
// Update NFS export policy for this connected host when the pool protocol is NFS3.
105+
updateNfsExportPolicyForConnectedHostIfNeeded(poolId, hostId, host, detailsMap);
106+
86107
// Create the ModifyStoragePoolCommand to send to the agent
87108
// Note: Always send command even if database entry exists, because agent may have restarted
88109
// and lost in-memory pool registration. The command handler is idempotent.
@@ -118,19 +139,19 @@ public boolean hostConnect(long hostId, long poolId) {
118139
}
119140

120141
String localPath = poolInfo.getLocalPath();
121-
logger.info("Storage pool {} successfully mounted at: {}", pool.getName(), localPath);
142+
logger.info("hostConnect: Storage pool {} successfully mounted at: {}", pool.getName(), localPath);
122143

123144
// Update or create the storage_pool_host_ref entry with the correct local_path
124145
StoragePoolHostVO storagePoolHost = storagePoolHostDao.findByPoolHost(poolId, hostId);
125146

126147
if (storagePoolHost == null) {
127148
storagePoolHost = new StoragePoolHostVO(poolId, hostId, localPath);
128149
storagePoolHostDao.persist(storagePoolHost);
129-
logger.info("Created storage_pool_host_ref entry for pool {} and host {}", pool.getName(), host.getName());
150+
logger.info("hostConnect: Created storage_pool_host_ref entry for pool {} and host {}", pool.getName(), host.getName());
130151
} else {
131152
storagePoolHost.setLocalPath(localPath);
132153
storagePoolHostDao.update(storagePoolHost.getId(), storagePoolHost);
133-
logger.info("Updated storage_pool_host_ref entry with local_path: {}", localPath);
154+
logger.info("hostConnect: Updated storage_pool_host_ref entry with local_path: {}", localPath);
134155
}
135156

136157
// Update pool capacity/usage information
@@ -139,62 +160,127 @@ public boolean hostConnect(long hostId, long poolId) {
139160
poolVO.setCapacityBytes(poolInfo.getCapacityBytes());
140161
poolVO.setUsedBytes(poolInfo.getCapacityBytes() - poolInfo.getAvailableBytes());
141162
_storagePoolDao.update(poolVO.getId(), poolVO);
142-
logger.info("Updated storage pool capacity: {} GB, used: {} GB", poolInfo.getCapacityBytes() / (1024 * 1024 * 1024), (poolInfo.getCapacityBytes() - poolInfo.getAvailableBytes()) / (1024 * 1024 * 1024));
163+
logger.info("hostConnect: Updated storage pool capacity: {} GB, used: {} GB", poolInfo.getCapacityBytes() / (1024 * 1024 * 1024), (poolInfo.getCapacityBytes() - poolInfo.getAvailableBytes()) / (1024 * 1024 * 1024));
143164
}
144165

145166
} catch (Exception e) {
146-
logger.error("Exception while connecting host {} to storage pool {}", host.getName(), pool.getName(), e);
167+
logger.error("hostConnect: Exception while connecting host {} to storage pool {}", host.getName(), pool.getName(), e);
147168
// CRITICAL: Don't throw exception - it crashes the agent and causes restart loops
148169
// Return false to indicate failure without crashing
149170
return false;
150171
}
151172
return true;
152173
}
153174

154-
@Override
155-
public boolean hostDisconnected(long hostId, long poolId) {
156-
logger.info("Disconnect from host " + hostId + " from pool " + poolId);
175+
private void updateNfsExportPolicyForConnectedHostIfNeeded(long poolId, long hostId, Host host, Map<String, String> detailsMap) {
176+
if (!ProtocolType.NFS3.name().equalsIgnoreCase(detailsMap.get(OntapStorageConstants.PROTOCOL))) {
177+
return;
178+
}
157179

158-
Host hostToremove = _hostDao.findById(hostId);
159-
if (hostToremove == null) {
160-
logger.error("Failed to add host by HostListener as host was not found with id : {}", hostId);
161-
return false;
180+
if (host == null) {
181+
throw new CloudRuntimeException("Host was not found with id: " + hostId);
162182
}
163183

164-
StoragePool pool = _storagePoolDao.findById(poolId);
165-
if (pool == null) {
166-
logger.error("Failed to disconnect host - storage pool not found with id: {}", poolId);
184+
if (!isNfs3EnabledOnHost(host)) {
185+
throw new CloudRuntimeException("NFS protocol is not enabled on host with id: " + hostId);
186+
}
187+
188+
AccessGroup accessGroup = new AccessGroup();
189+
accessGroup.setStoragePoolId(poolId);
190+
accessGroup.setHostsToConnect(List.of((HostVO) host));
191+
192+
StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(detailsMap);
193+
strategy.updateAccessGroup(accessGroup);
194+
logger.info("hostConnect: updateNfsExportPolicyForConnectedHostIfNeeded: Updated NFS export policy rules for host {} on storage pool {}", host.getName(), poolId);
195+
}
196+
197+
private boolean isNfs3EnabledOnHost(Host host) {
198+
if (host == null) {
167199
return false;
168200
}
169-
logger.info("Disconnecting host {} from ONTAP storage pool {}", hostToremove.getName(), pool.getName());
170201

171-
try {
172-
DeleteStoragePoolCommand cmd = new DeleteStoragePoolCommand(pool);
173-
Answer answer = _agentMgr.easySend(hostId, cmd);
174-
if (answer != null && answer.getResult()) {
175-
logger.info("Successfully disconnected host {} from ONTAP storage pool {}", hostToremove.getName(), pool.getName());
176-
return true;
177-
} else {
178-
String errMsg = (answer != null) ? answer.getDetails() : "Unknown error";
179-
logger.warn("Failed to disconnect host {} from storage pool {}. Error: {}", hostToremove.getName(), pool.getName(), errMsg);
180-
return false;
181-
}
182-
} catch (Exception e) {
183-
logger.error("Exception while disconnecting host {} from storage pool {}", hostToremove.getName(), pool.getName(), e);
202+
String storageIp = host.getStorageIpAddress() != null ? host.getStorageIpAddress().trim() : "";
203+
if (storageIp.isEmpty() && StringUtils.isBlank(host.getPrivateIpAddress())) {
204+
logger.warn("isNfs3EnabledOnHost: Host {} is not eligible for NFS3 protocol: both storage IP and private IP are empty",
205+
host.getId());
184206
return false;
185207
}
208+
209+
return true;
186210
}
187211

188212
@Override
189-
public boolean hostAboutToBeRemoved(long hostId) {
213+
public boolean hostDisconnected(long hostId, long poolId) {
214+
logger.info("hostDisconnected: Disconnecting host {} from pool {}", hostId, poolId);
215+
// Note: This is not currently being called for NetApp ONTAP storage plugin.
190216
return false;
191217
}
192218

219+
@Override
220+
public boolean hostAboutToBeRemoved(long hostId) {
221+
logger.info("hostAboutToBeRemoved: Host {} is about to be removed", hostId);
222+
223+
Host host = _hostDao.findById(hostId);
224+
if (host == null) {
225+
logger.warn("hostAboutToBeRemoved: Host not found with id: {}, considering it as no-op", hostId);
226+
return true;
227+
}
228+
229+
List<StoragePoolHostVO> poolHostRefs = storagePoolHostDao.listByHostId(hostId);
230+
if (poolHostRefs == null || poolHostRefs.isEmpty()) {
231+
logger.debug("hostAboutToBeRemoved: No storage pool associations found for host {}", hostId);
232+
return true;
233+
}
234+
235+
for (StoragePoolHostVO ref : poolHostRefs) {
236+
StoragePoolVO pool = _storagePoolDao.findById(ref.getPoolId());
237+
if (pool != null) {
238+
removeHostFromOntapPoolIfNeeded(pool, host);
239+
}
240+
}
241+
242+
logger.info("hostAboutToBeRemoved: Cleaned up ONTAP export policies for host {} about to be removed", hostId);
243+
return true;
244+
}
245+
193246
@Override
194247
public boolean hostRemoved(long hostId, long clusterId) {
195248
return false;
196249
}
197250

251+
private void removeHostFromOntapPoolIfNeeded(StoragePoolVO pool, Host host) {
252+
try {
253+
Map<String, String> detailsMap = _storagePoolDetailsDao.listDetailsKeyPairs(pool.getId());
254+
if (detailsMap == null || detailsMap.isEmpty()) {
255+
logger.debug("hostAboutToBeRemoved: removeHostFromOntapPoolIfNeeded: No pool details found for pool id: {}", pool.getId());
256+
return;
257+
}
258+
259+
// Skip non-NFS3 pools; Currently, for iSCSI type, iGroup rules are being handled as part of revokeAccess in OntapPrimaryDataStoreDriver, so no need to handle here.
260+
if (!ProtocolType.NFS3.name().equalsIgnoreCase(detailsMap.get(OntapStorageConstants.PROTOCOL))) {
261+
return;
262+
}
263+
264+
logger.info("hostAboutToBeRemoved: removeHostFromOntapPoolIfNeeded: Removing export policy rule for host {} from storage pool {}", host.getName(), pool.getName());
265+
if (!isNfs3EnabledOnHost(host)) {
266+
logger.warn("hostAboutToBeRemoved: removeHostFromOntapPoolIfNeeded: Skipping NFS export policy removal for host {} on pool {} as host is not NFS-enabled",
267+
host.getId(), pool.getId());
268+
return;
269+
}
270+
AccessGroup accessGroup = new AccessGroup();
271+
accessGroup.setStoragePoolId(pool.getId());
272+
accessGroup.setHostsToConnect(List.of((HostVO) host));
273+
accessGroup.setHostRuleAction(AccessGroup.HostRuleAction.REMOVE);
274+
275+
StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(detailsMap);
276+
strategy.updateAccessGroup(accessGroup);
277+
logger.info("hostAboutToBeRemoved: removeHostFromOntapPoolIfNeeded: Removed NFS export policy rules for removed host {} from storage pool {}", host.getName(), pool.getName());
278+
} catch (Exception e) {
279+
logger.warn("hostAboutToBeRemoved: removeHostFromOntapPoolIfNeeded: Failed to remove NFS export policy rule for host {} from pool {}: {}", host.getId(), pool.getName(), e.getMessage());
280+
// Continue processing other pools even if one fails
281+
}
282+
}
283+
198284
@Override
199285
public boolean hostEnabled(long hostId) {
200286
return false;

0 commit comments

Comments
 (0)