Skip to content

Commit 8323d52

Browse files
Addressed comments
1 parent e548577 commit 8323d52

2 files changed

Lines changed: 51 additions & 63 deletions

File tree

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

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,12 @@
6565
import com.cloud.agent.api.to.DataStoreTO;
6666
import com.cloud.agent.api.to.DataTO;
6767
import com.cloud.exception.InvalidParameterValueException;
68-
import com.cloud.exception.StorageConflictException;
69-
import com.cloud.exception.StorageUnavailableException;
7068
import com.cloud.host.Host;
7169
import com.cloud.host.HostVO;
7270
import com.cloud.storage.ScopeType;
7371
import com.cloud.storage.Storage;
7472
import com.cloud.storage.StorageManager;
7573
import com.cloud.storage.StoragePool;
76-
import com.cloud.storage.StoragePoolHostVO;
7774
import com.cloud.storage.Volume;
7875
import com.cloud.storage.VolumeDetailVO;
7976
import com.cloud.storage.VolumeVO;
@@ -399,9 +396,8 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
399396
// Only retrieve LUN name for iSCSI volumes
400397
grantAccessIscsi(host, volumeVO, details, svmName, storagePool);
401398
} else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
402-
// For NFS, ensure export policy has host rule and host is connected to pool.
403-
ensureNfsHostAccess(host, storagePool, details);
404-
logger.debug("grantAccess: NFS volume [{}], ensured host access to storage pool [{}]", volumeVO.getUuid(), storagePool.getId());
399+
// For NFS, no access grant needed - file is accessible via mount
400+
logger.debug("grantAccess: NFS volume [{}], no igroup mapping required", volumeVO.getUuid());
405401
return true;
406402
}
407403
volumeVO.setPoolType(storagePool.getPoolType());
@@ -418,43 +414,6 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
418414
}
419415
}
420416

421-
private void ensureNfsHostAccess(Host host, StoragePoolVO storagePool, Map<String, String> details) {
422-
boolean hostConnectedToPool = false;
423-
List<StoragePoolHostVO> connectedPools = storageManager.findStoragePoolsConnectedToHost(host.getId());
424-
if (connectedPools != null) {
425-
for (StoragePoolHostVO connectedPoolRef : connectedPools) {
426-
if (connectedPoolRef.getPoolId() == storagePool.getId()) {
427-
hostConnectedToPool = true;
428-
break;
429-
}
430-
}
431-
}
432-
433-
if (!hostConnectedToPool) {
434-
try {
435-
logger.info("grantAccess: Host [{}] is not connected to NFS pool [{}], reconnecting host to pool", host.getId(), storagePool.getId());
436-
boolean connected = storageManager.connectHostToSharedPool(host, storagePool.getId());
437-
if (!connected) {
438-
throw new CloudRuntimeException("Failed to connect host " + host.getId() + " to NFS pool " + storagePool.getId());
439-
}
440-
} catch (StorageUnavailableException | StorageConflictException e) {
441-
throw new CloudRuntimeException("Unable to connect host " + host.getId() + " to NFS pool " + storagePool.getId(), e);
442-
}
443-
return;
444-
}
445-
446-
if (!(host instanceof HostVO)) {
447-
throw new CloudRuntimeException("Host object is not of type HostVO for host id: " + host.getId());
448-
}
449-
450-
AccessGroup accessGroup = new AccessGroup();
451-
accessGroup.setStoragePoolId(storagePool.getId());
452-
accessGroup.setHostsToConnect(List.of((HostVO) host));
453-
454-
StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
455-
strategy.updateAccessGroup(accessGroup);
456-
}
457-
458417
private void grantAccessIscsi(Host host, VolumeVO volumeVO, Map<String, String> details, String svmName, StoragePoolVO storagePool) {
459418
String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME).getValue();
460419
UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details);

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -201,79 +201,105 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) {
201201
if (accessGroup == null) {
202202
throw new CloudRuntimeException("Invalid accessGroup object - accessGroup is null");
203203
}
204+
// Check if an AccessGroup was constructed without associating it to a storage pool.
204205
if (accessGroup.getStoragePoolId() == null) {
205206
throw new CloudRuntimeException("Invalid accessGroup object - storagePoolId is null");
206207
}
208+
// At least one host is required regardless of ADD or REMOVE action.
209+
// An empty list means there is nothing to add to or remove from the export policy client list.
207210
if (accessGroup.getHostsToConnect() == null || accessGroup.getHostsToConnect().isEmpty()) {
208211
throw new CloudRuntimeException("Invalid accessGroup object - hostsToConnect is null or empty");
209212
}
210213

211214
Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId());
215+
if (details == null || details.isEmpty()) {
216+
throw new CloudRuntimeException("No storage pool details found for storagePoolId: " + accessGroup.getStoragePoolId());
217+
}
212218
String exportPolicyId = details.get(OntapStorageConstants.EXPORT_POLICY_ID);
213-
214-
// No policy exists yet (e.g. pool was created before any eligible hosts came up), create it now.
215219
if (exportPolicyId == null || exportPolicyId.isEmpty()) {
216-
logger.info("updateAccessGroup: No export policy found for pool {}. Creating one now.", accessGroup.getStoragePoolId());
217-
return createAccessGroup(accessGroup);
220+
throw new CloudRuntimeException("No export policy found for storagePoolId: " + accessGroup.getStoragePoolId());
218221
}
219222

223+
220224
try {
221225
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
222226
ExportPolicy existingPolicy = nasFeignClient.getExportPolicyById(authHeader, exportPolicyId);
227+
// Check if the export policy was deleted externally on ONTAP or the stored ID is stale.
223228
if (existingPolicy == null) {
224229
throw new CloudRuntimeException("Failed to fetch existing export policy with id: " + exportPolicyId);
225230
}
226231

227232
List<ExportRule> rules = existingPolicy.getRules();
233+
// An existing export policy should always have at least one rule. A null or empty rules list
234+
// indicates the policy was corrupted or modified externally on ONTAP and is in an unexpected
235+
// state. updateAccessGroup only modifies an existing policy — it does not create rules from scratch.
228236
if (rules == null || rules.isEmpty()) {
229-
rules = new ArrayList<>();
230-
ExportRule newRule = new ExportRule();
231-
newRule.setProtocols(List.of(ExportRule.ProtocolsEnum.NFS3));
232-
newRule.setRoRule(List.of("sys"));
233-
newRule.setRwRule(List.of("sys"));
234-
newRule.setSuperuser(List.of("sys"));
235-
newRule.setClients(new ArrayList<>());
236-
rules.add(newRule);
237+
throw new CloudRuntimeException("Export policy " + existingPolicy.getName() + " has no rules. " +
238+
"Cannot update an export policy with no existing rules.");
237239
}
238240

239-
ExportRule exportRule = rules.get(0);
240-
List<ExportRule.ExportClient> exportClients = exportRule.getClients();
241-
if (exportClients == null) {
242-
exportClients = new ArrayList<>();
243-
exportRule.setClients(exportClients);
241+
// This plugin creates a single NFS export rule per policy. More than one rule means the
242+
// policy was changed outside the plugin and we no longer know which rule should be mutated.
243+
if (rules.size() != 1) {
244+
throw new CloudRuntimeException("Export policy " + existingPolicy.getName() +
245+
" is expected to have exactly one rule but found: " + rules.size());
244246
}
245247

248+
ExportRule targetRule = rules.get(0);
249+
246250
Set<String> hostMatches = new HashSet<>();
247251
for (HostVO host : accessGroup.getHostsToConnect()) {
248252
String hostStorageIp = host.getStorageIpAddress();
249253
String ip = (hostStorageIp != null && !hostStorageIp.isEmpty()) ? hostStorageIp : host.getPrivateIpAddress();
254+
// Occurs when a CloudStack host has neither a storage IP nor a private IP configured
255+
// (misconfigured or partially registered host). Skip it to avoid inserting a broken
256+
// or empty match entry into the ONTAP export rule.
250257
if (ip == null || ip.isEmpty()) {
251258
logger.warn("updateAccessGroup: Host {} has no storage/private IP, skipping export rule update", host.getName());
252259
continue;
253260
}
254261
hostMatches.add(ip + "/32");
255262
}
256263

264+
// Occurs when every host in hostsToConnect had no valid IP (all were skipped above).
265+
// There is nothing to add or remove, so skip the ONTAP API call and return early.
257266
if (hostMatches.isEmpty()) {
258267
accessGroup.setPolicy(existingPolicy);
259268
return accessGroup;
260269
}
261270

262271
boolean updated = false;
272+
// Differentiates between removing hosts (e.g., host decommissioned or removed from the cluster)
273+
// and the default ADD path (e.g., new host being connected to the storage pool).
274+
List<ExportRule.ExportClient> exportClients = targetRule.getClients();
275+
// Existing rules can legitimately have no clients yet; treat that as an empty list.
276+
if (exportClients == null) {
277+
exportClients = new ArrayList<>();
278+
targetRule.setClients(exportClients);
279+
}
280+
263281
if (AccessGroup.HostRuleAction.REMOVE.equals(accessGroup.getHostRuleAction())) {
264282
updated = exportClients.removeIf(c -> c != null && c.getMatch() != null && hostMatches.contains(c.getMatch()));
283+
// None of the requested host IPs were present in the policy — log for diagnostics
284+
// so operators can investigate whether the policy state is already correct or stale.
265285
if (!updated) {
266286
logger.info("updateAccessGroup: No matching host IPs found in export policy {} for removal", existingPolicy.getName());
267287
}
268288
} else {
269289
Set<String> existingMatches = new HashSet<>();
270290
for (ExportRule.ExportClient exportClient : exportClients) {
291+
// Skips null client entries or entries with a null match field that may have been
292+
// inserted externally on ONTAP. Avoids polluting the dedup set with null values
293+
// which would cause subsequent hosts to be incorrectly treated as duplicates.
271294
if (exportClient != null && exportClient.getMatch() != null) {
272295
existingMatches.add(exportClient.getMatch());
273296
}
274297
}
275298

276299
for (String match : hostMatches) {
300+
// Set.add() returns false when the element was already present, acting as a dedup check.
301+
// Prevents inserting a duplicate client match entry for a host that is already allowed
302+
// in the export policy — ONTAP may reject or behave unpredictably with duplicate matches.
277303
if (existingMatches.add(match)) {
278304
ExportRule.ExportClient exportClient = new ExportRule.ExportClient();
279305
exportClient.setMatch(match);
@@ -283,13 +309,16 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) {
283309
}
284310
}
285311

312+
// Occurs when the export policy is already in the desired state:
313+
// ADD path — all provided host IPs were already present (all were duplicates).
314+
// REMOVE path — none of the provided host IPs matched any existing entry.
315+
// In both cases, skip the ONTAP PATCH call to avoid an unnecessary round-trip.
286316
if (!updated) {
317+
// Only log the "nothing to add" message for the ADD path; the REMOVE no-op
318+
// is already logged above in its own branch to avoid double-logging.
287319
if (!AccessGroup.HostRuleAction.REMOVE.equals(accessGroup.getHostRuleAction())) {
288320
logger.info("updateAccessGroup: No new host IPs to add to export policy {}", existingPolicy.getName());
289321
}
290-
}
291-
292-
if (!updated) {
293322
accessGroup.setPolicy(existingPolicy);
294323
return accessGroup;
295324
}

0 commit comments

Comments
 (0)