nvazquez commented on code in PR #9478: URL: https://github.com/apache/cloudstack/pull/9478#discussion_r2126886200
########## api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java: ########## @@ -84,6 +85,19 @@ public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd { "Do not specify destzoneid and destzoneids together, however one of them is required.") protected List<Long> destZoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, Review Comment: Can maybe add the since attribute for these new parameters ########## api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java: ########## @@ -106,7 +120,18 @@ public List<Long> getDestinationZoneIds() { destIds.add(destZoneId); return destIds; } - return null; + return new ArrayList<>(); + } + + public List<Long> getStoragePoolIds() { + return storagePoolIds; + } + + public Boolean useStorageReplication() { + if (useStorageReplication == null) { + return false; + } + return useStorageReplication; Review Comment: Minor one: can reduce the implementation by returning `BooleanUtils.toBoolean(useStorageReplication)` and make the method return a boolean instead (to avoid null checks) ########## api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java: ########## @@ -161,6 +176,17 @@ public List<Long> getZoneIds() { return zoneIds; } + public List<Long> getStoragePoolIds() { + return storagePoolIds; Review Comment: Maybe returning an empty list when the parameter is not set? ########## api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java: ########## @@ -99,6 +101,19 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd { since = "4.19.0") protected List<Long> zoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = StoragePoolResponse.class, + authorized = RoleType.Admin, + description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + + "The snapshot will always be made available in the zone in which the volume is present.", + since = "4.20.0") Review Comment: Now moved to 4.21 :) ########## api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java: ########## @@ -62,9 +61,14 @@ public class SnapshotPolicyResponse extends BaseResponseWithTagInformation { @Param(description = "The list of zones in which snapshot backup is scheduled", responseObject = ZoneResponse.class, since = "4.19.0") protected Set<ZoneResponse> zones; + @SerializedName(ApiConstants.STORAGE) + @Param(description = "The list of pools in which snapshot backup is scheduled", responseObject = StoragePoolResponse.class, since = "4.20.0") Review Comment: Also here ########## engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java: ########## @@ -591,7 +595,7 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use } // don't try to perform a sync if the DataStoreRole of the snapshot is equal to DataStoreRole.Primary - if (!DataStoreRole.Primary.equals(dataStoreRole) || kvmSnapshotOnlyInPrimaryStorage) { + if (!DataStoreRole.Primary.equals(dataStoreRole) || !keepOnPrimary) { Review Comment: Maybe I'm not following the logic but the names of these variables look similar, but have opposite meanings. Would it be fair to rename the one named `keepOnPrimary` to `storageSupportSnapshotToTemplateEnabled`? ########## server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java: ########## @@ -2155,6 +2289,73 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep } } + private boolean canCopyOnPrimary(List<Long> poolIds, Snapshot snapshot) { + List<Long> poolsToBeRemoved = new ArrayList<>(); + for (Long poolId : poolIds) { + PrimaryDataStore dataStore = (PrimaryDataStore) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (isObjectNull(dataStore == null, poolsToBeRemoved, poolId)) continue; + + SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snapshot.getId(), poolId, DataStoreRole.Primary); + if (isSnapshotExistsOnPool(snapshot, dataStore, snapshotInfo)) continue; + + VolumeVO volume = _volsDao.findById(snapshot.getVolumeId()); + if (isObjectNull(volume == null, poolsToBeRemoved, poolId)) continue; + doesStorageSupportCopySnapshot(poolsToBeRemoved, poolId, dataStore, volume); + } + poolIds.removeAll(poolsToBeRemoved); + if (CollectionUtils.isEmpty(poolIds)) { + return false; + } + return true; + } + + private void doesStorageSupportCopySnapshot(List<Long> poolsToBeRemoved, Long poolId, PrimaryDataStore dataStore, VolumeVO volume) { + if (!dataStore.getDriver().getCapabilities() + .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString()) + && dataStore.getPoolType() != volume.getPoolType()) { + poolsToBeRemoved.add(poolId); + logger.debug(String.format("The %s does not support copy to %s between zones", dataStore.getPoolType(), volume.getPoolType())); + } + } + + private boolean isSnapshotExistsOnPool(Snapshot snapshot, PrimaryDataStore dataStore, SnapshotInfo snapshotInfo) { + if (snapshotInfo != null) { + logger.debug(String.format("Snapshot [%s] already exist on pool [%s]", snapshot.getUuid(), dataStore.getName())); + return true; + } + return false; + } + + private static boolean isObjectNull(boolean object, List<Long> poolsToBeRemoved, Long poolId) { Review Comment: Can this method be renamed? By its name it would be assumed its just a null check but its doing more than that ########## server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java: ########## @@ -2155,6 +2289,73 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep } } + private boolean canCopyOnPrimary(List<Long> poolIds, Snapshot snapshot) { + List<Long> poolsToBeRemoved = new ArrayList<>(); + for (Long poolId : poolIds) { + PrimaryDataStore dataStore = (PrimaryDataStore) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (isObjectNull(dataStore == null, poolsToBeRemoved, poolId)) continue; + + SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snapshot.getId(), poolId, DataStoreRole.Primary); + if (isSnapshotExistsOnPool(snapshot, dataStore, snapshotInfo)) continue; + + VolumeVO volume = _volsDao.findById(snapshot.getVolumeId()); + if (isObjectNull(volume == null, poolsToBeRemoved, poolId)) continue; + doesStorageSupportCopySnapshot(poolsToBeRemoved, poolId, dataStore, volume); + } + poolIds.removeAll(poolsToBeRemoved); + if (CollectionUtils.isEmpty(poolIds)) { + return false; + } + return true; + } + + private void doesStorageSupportCopySnapshot(List<Long> poolsToBeRemoved, Long poolId, PrimaryDataStore dataStore, VolumeVO volume) { + if (!dataStore.getDriver().getCapabilities() Review Comment: To avoid NPEs, can you check `dataStore.getDriver() != null` and `MapUtils.isNotEmpty(dataStore.getDriver().getCapabilities())`? ########## server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java: ########## @@ -1650,10 +1709,47 @@ public boolean isHypervisorKvmAndFileBasedStorage(VolumeInfo volumeInfo, Storage return volumeInfo.getHypervisorType() == HypervisorType.KVM && fileBasedStores.contains(storagePool.getPoolType()); } + private void copyNewSnapshotToZonesOnPrimary(CreateSnapshotPayload payload, SnapshotInfo snapshot) { + SnapshotStrategy snapshotStrategy; + snapshotStrategy = _storageStrategyFactory.getSnapshotStrategy(snapshot, SnapshotOperation.COPY); + if (snapshotStrategy != null) { + for (Long storagePoolId : payload.getStoragePoolIds()) { + copySnapshotOnPool(snapshot, snapshotStrategy, storagePoolId); + } + } else { + logger.info("Unable to find snapshot strategy to handle the copy of a snapshot with id " + snapshot.getUuid()); + } + } + + private boolean copySnapshotOnPool(SnapshotInfo snapshot, SnapshotStrategy snapshotStrategy, Long storagePoolId) { + DataStore store = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); + SnapshotInfo snapshotOnStore = (SnapshotInfo) store.create(snapshot); + + try { + AsyncCallFuture<SnapshotResult> future = snapshotSrv.copySnapshot(snapshot, snapshotOnStore, snapshotStrategy); + SnapshotResult result = future.get(); + if (result.isFailed()) { + logger.debug(String.format("Copy snapshot ID: %d failed for primary storage %s: %s", snapshot.getSnapshotId(), storagePoolId, result.getResult())); + return false; + } + snapshotZoneDao.addSnapshotToZone(snapshot.getId(), snapshotOnStore.getDataCenterId()); + _resourceLimitMgr.incrementResourceCount(CallContext.current().getCallingUserId(), ResourceType.primary_storage, snapshot.getSize()); + if (CallContext.current().getCallingUserId() != Account.ACCOUNT_ID_SYSTEM) { + SnapshotVO snapshotVO = _snapshotDao.findByIdIncludingRemoved(snapshot.getSnapshotId()); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_COPY, CallContext.current().getCallingUserId(), snapshotOnStore.getDataCenterId(), snapshotVO.getId(), null, null, null, snapshotVO.getSize(), + snapshotVO.getSize(), snapshotVO.getClass().getName(), snapshotVO.getUuid()); + } + } catch (InterruptedException e) { Review Comment: Maybe can collapse both on a single catch block and log the exception as well? ########## api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotPolicyCmd.java: ########## @@ -83,6 +83,14 @@ public class CreateSnapshotPolicyCmd extends BaseCmd { "The snapshots will always be made available in the zone in which the volume is present.") protected List<Long> zoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = StoragePoolResponse.class, + description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + + "The snapshot will always be made available in the zone in which the volume is present.", + since = "4.20.0") Review Comment: Also here ########## server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java: ########## @@ -2122,16 +2226,42 @@ protected DataCenterVO getCheckedDestinationZoneForSnapshotCopy(long zoneId, boo return dstZone; } + protected StoragePoolVO getCheckedDestinationStorageForSnapshotCopy(long poolId, boolean isRootAdmin) { + StoragePoolVO destPool = _storagePoolDao.findById(poolId); + if (destPool == null) { + throw new InvalidParameterValueException("Please specify a valid destination zone."); + } + if (!StoragePoolStatus.Up.equals(destPool.getStatus()) && !isRootAdmin) { + throw new PermissionDeniedException("Cannot perform this operation, the storage pool is not in Up state: " + destPool.getName()); Review Comment: The message is missing something like: "or not the ROOT admin" ########## server/src/main/java/com/cloud/api/ApiDBUtils.java: ########## @@ -1707,6 +1706,19 @@ public static List<DataCenterVO> findSnapshotPolicyZones(SnapshotPolicy policy, return s_zoneDao.listByIds(zoneIds); } + public static List<StoragePoolVO> findSnapshotPolicyPools(SnapshotPolicy policy, Volume volume) { + List<SnapshotPolicyDetailVO> poolDetails = s_snapshotPolicyDetailsDao.findDetails(policy.getId(), ApiConstants.STORAGE_ID); + List<Long> poolIds = new ArrayList<>(); + for (SnapshotPolicyDetailVO detail : poolDetails) { + try { + poolIds.add(Long.valueOf(detail.getValue())); + } catch (NumberFormatException ignored) {} Review Comment: I think this should be logged in case there is an exception -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: commits-unsubscr...@cloudstack.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org