Skip to content

Commit 6932cac

Browse files
Allow copy of templates from secondary storages of other zone when adding a new secondary storage (#12296)
* Allow copy of templates from secondary storages of other zone when adding a new secondary storage * Add API param and UI changes on add secondary storage page * Make copy template across zones non blocking * Code fixes * unused imports * Add copy template flag in zone wizard and remove NFS checks * Fix UI * Label fixes * code optimizations * code refactoring * missing changes * Combine template copy and download into a single asynchronous operation * unused import and fixed conflicts * unused code * update config message * Fix configuration setting value on add secondary storage page * Removed unused code * Update unit tests
1 parent 0dcbe57 commit 6932cac

File tree

14 files changed

+487
-65
lines changed

14 files changed

+487
-65
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
import com.cloud.exception.DiscoveryException;
3030
import com.cloud.storage.ImageStore;
3131
import com.cloud.user.Account;
32+
import org.apache.commons.collections.MapUtils;
33+
34+
import java.util.Collection;
35+
import java.util.HashMap;
36+
import java.util.Map;
3237

3338
@APICommand(name = "addSecondaryStorage", description = "Adds secondary storage.", responseObject = ImageStoreResponse.class,
3439
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@@ -44,6 +49,9 @@ public class AddSecondaryStorageCmd extends BaseCmd {
4449
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, description = "The Zone ID for the secondary storage")
4550
protected Long zoneId;
4651

52+
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].copytemplatesfromothersecondarystorages=true")
53+
protected Map details;
54+
4755
/////////////////////////////////////////////////////
4856
/////////////////// Accessors ///////////////////////
4957
/////////////////////////////////////////////////////
@@ -56,6 +64,20 @@ public Long getZoneId() {
5664
return zoneId;
5765
}
5866

67+
public Map<String, String> getDetails() {
68+
Map<String, String> detailsMap = new HashMap<>();
69+
if (MapUtils.isNotEmpty(details)) {
70+
Collection<?> props = details.values();
71+
for (Object prop : props) {
72+
HashMap<String, String> detail = (HashMap<String, String>) prop;
73+
for (Map.Entry<String, String> entry: detail.entrySet()) {
74+
detailsMap.put(entry.getKey(),entry.getValue());
75+
}
76+
}
77+
}
78+
return detailsMap;
79+
}
80+
5981
/////////////////////////////////////////////////////
6082
/////////////// API Implementation///////////////////
6183
/////////////////////////////////////////////////////
@@ -68,7 +90,7 @@ public long getEntityOwnerId() {
6890
@Override
6991
public void execute(){
7092
try{
71-
ImageStore result = _storageService.discoverImageStore(null, getUrl(), "NFS", getZoneId(), null);
93+
ImageStore result = _storageService.discoverImageStore(null, getUrl(), "NFS", getZoneId(), getDetails());
7294
ImageStoreResponse storeResponse = null;
7395
if (result != null ) {
7496
storeResponse = _responseGenerator.createImageStoreResponse(result);

engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import org.apache.cloudstack.api.response.MigrationResponse;
2424
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
25-
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
2625
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
2726
import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy;
2827

@@ -31,5 +30,5 @@ public interface StorageOrchestrationService {
3130

3231
MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreId, List<Long> templateIdList, List<Long> snapshotIdList);
3332

34-
Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore);
33+
Future<TemplateApiResult> orchestrateTemplateCopyFromSecondaryStores(long templateId, DataStore destStore);
3534
}

engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,6 @@ public TemplateInfo getTemplate() {
8080
List<DatadiskTO> getTemplateDatadisksOnImageStore(TemplateInfo templateInfo, String configurationId);
8181

8282
AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject source, DataStore destStore);
83-
}
83+
84+
void handleTemplateCopyFromSecondaryStores(long templateId, DataStore destStore);
85+
}

engine/components-api/src/main/java/com/cloud/storage/StorageManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,9 @@ public interface StorageManager extends StorageService {
220220
"storage.pool.host.connect.workers", "1",
221221
"Number of worker threads to be used to connect hosts to a primary storage", true);
222222

223-
ConfigKey<Boolean> COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES = new ConfigKey<>(Boolean.class, "copy.public.templates.from.other.storages",
224-
"Storage", "true", "Allow SSVMs to try copying public templates from one secondary storage to another instead of downloading them from the source.",
223+
ConfigKey<Boolean> COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES = new ConfigKey<>(Boolean.class, "copy.templates.from.other.secondary.storages",
224+
"Storage", "true", "When enabled, this feature allows templates to be copied from existing Secondary Storage servers (within the same zone or across zones) " +
225+
"while adding a new Secondary Storage. If the copy operation fails, the system falls back to downloading the template from the source URL.",
225226
true, ConfigKey.Scope.Zone, null);
226227

227228
/**

engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
import javax.inject.Inject;
3737
import javax.naming.ConfigurationException;
3838

39+
import com.cloud.dc.dao.DataCenterDao;
40+
import com.cloud.storage.dao.VMTemplateDao;
41+
import com.cloud.template.TemplateManager;
3942
import org.apache.cloudstack.api.response.MigrationResponse;
4043
import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
4144
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
@@ -45,6 +48,7 @@
4548
import org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageService.DataObjectResult;
4649
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
4750
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
51+
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
4852
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
4953
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService;
5054
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
@@ -103,6 +107,15 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
103107
VolumeDataStoreDao volumeDataStoreDao;
104108
@Inject
105109
DataMigrationUtility migrationHelper;
110+
@Inject
111+
TemplateManager templateManager;
112+
@Inject
113+
VMTemplateDao templateDao;
114+
@Inject
115+
TemplateDataFactory templateDataFactory;
116+
@Inject
117+
DataCenterDao dcDao;
118+
106119

107120
ConfigKey<Double> ImageStoreImbalanceThreshold = new ConfigKey<>("Advanced", Double.class,
108121
"image.store.imbalance.threshold",
@@ -304,8 +317,9 @@ public MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreI
304317
}
305318

306319
@Override
307-
public Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore) {
308-
return submit(destStore.getScope().getScopeId(), new CopyTemplateTask(source, destStore));
320+
public Future<TemplateApiResult> orchestrateTemplateCopyFromSecondaryStores(long srcTemplateId, DataStore destStore) {
321+
Long dstZoneId = destStore.getScope().getScopeId();
322+
return submit(dstZoneId, new CopyTemplateFromSecondaryStorageTask(srcTemplateId, destStore));
309323
}
310324

311325
protected Pair<String, Boolean> migrateCompleted(Long destDatastoreId, DataStore srcDatastore, List<DataObject> files, MigrationPolicy migrationPolicy, int skipped) {
@@ -624,13 +638,13 @@ public DataObjectResult call() {
624638
}
625639
}
626640

627-
private class CopyTemplateTask implements Callable<TemplateApiResult> {
628-
private TemplateInfo sourceTmpl;
629-
private DataStore destStore;
630-
private String logid;
641+
private class CopyTemplateFromSecondaryStorageTask implements Callable<TemplateApiResult> {
642+
private final long srcTemplateId;
643+
private final DataStore destStore;
644+
private final String logid;
631645

632-
public CopyTemplateTask(TemplateInfo sourceTmpl, DataStore destStore) {
633-
this.sourceTmpl = sourceTmpl;
646+
CopyTemplateFromSecondaryStorageTask(long srcTemplateId, DataStore destStore) {
647+
this.srcTemplateId = srcTemplateId;
634648
this.destStore = destStore;
635649
this.logid = ThreadContext.get(LOGCONTEXTID);
636650
}
@@ -639,17 +653,16 @@ public CopyTemplateTask(TemplateInfo sourceTmpl, DataStore destStore) {
639653
public TemplateApiResult call() {
640654
ThreadContext.put(LOGCONTEXTID, logid);
641655
TemplateApiResult result;
642-
AsyncCallFuture<TemplateApiResult> future = templateService.copyTemplateToImageStore(sourceTmpl, destStore);
656+
long destZoneId = destStore.getScope().getScopeId();
657+
TemplateInfo sourceTmpl = templateDataFactory.getTemplate(srcTemplateId, DataStoreRole.Image);
643658
try {
644-
result = future.get();
645-
} catch (ExecutionException | InterruptedException e) {
646-
logger.warn("Exception while copying template [{}] from image store [{}] to image store [{}]: {}",
647-
sourceTmpl.getUniqueName(), sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
659+
templateService.handleTemplateCopyFromSecondaryStores(srcTemplateId, destStore);
648660
result = new TemplateApiResult(sourceTmpl);
649-
result.setResult(e.getMessage());
661+
} finally {
662+
tryCleaningUpExecutor(destZoneId);
663+
ThreadContext.clearAll();
650664
}
651-
tryCleaningUpExecutor(destStore.getScope().getScopeId());
652-
ThreadContext.clearAll();
665+
653666
return result;
654667
}
655668
}

engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java

Lines changed: 135 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
import javax.inject.Inject;
3333

34+
import com.cloud.exception.StorageUnavailableException;
35+
import org.apache.cloudstack.context.CallContext;
3436
import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
3537
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
3638
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
@@ -67,9 +69,11 @@
6769
import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
6870
import org.apache.cloudstack.storage.image.store.TemplateObject;
6971
import org.apache.cloudstack.storage.to.TemplateObjectTO;
72+
import org.apache.commons.collections.CollectionUtils;
7073
import org.apache.commons.lang3.StringUtils;
7174
import org.apache.logging.log4j.Logger;
7275
import org.apache.logging.log4j.LogManager;
76+
import org.apache.logging.log4j.ThreadContext;
7377
import org.springframework.stereotype.Component;
7478

7579
import com.cloud.agent.api.Answer;
@@ -567,10 +571,7 @@ public void handleTemplateSync(DataStore store) {
567571
}
568572

569573
if (availHypers.contains(tmplt.getHypervisorType())) {
570-
boolean copied = isCopyFromOtherStoragesEnabled(zoneId) && tryCopyingTemplateToImageStore(tmplt, store);
571-
if (!copied) {
572-
tryDownloadingTemplateToImageStore(tmplt, store);
573-
}
574+
storageOrchestrator.orchestrateTemplateCopyFromSecondaryStores(tmplt.getId(), store);
574575
} else {
575576
logger.info("Skip downloading template {} since current data center does not have hypervisor {}", tmplt, tmplt.getHypervisorType());
576577
}
@@ -617,6 +618,16 @@ public void handleTemplateSync(DataStore store) {
617618

618619
}
619620

621+
@Override
622+
public void handleTemplateCopyFromSecondaryStores(long templateId, DataStore destStore) {
623+
VMTemplateVO template = _templateDao.findById(templateId);
624+
long zoneId = destStore.getScope().getScopeId();
625+
boolean copied = imageStoreDetailsUtil.isCopyTemplatesFromOtherStoragesEnabled(destStore.getId(), zoneId) && tryCopyingTemplateToImageStore(template, destStore);
626+
if (!copied) {
627+
tryDownloadingTemplateToImageStore(template, destStore);
628+
}
629+
}
630+
620631
protected void tryDownloadingTemplateToImageStore(VMTemplateVO tmplt, DataStore destStore) {
621632
if (tmplt.getUrl() == null) {
622633
logger.info("Not downloading template [{}] to image store [{}], as it has no URL.", tmplt.getUniqueName(),
@@ -634,28 +645,134 @@ protected void tryDownloadingTemplateToImageStore(VMTemplateVO tmplt, DataStore
634645
}
635646

636647
protected boolean tryCopyingTemplateToImageStore(VMTemplateVO tmplt, DataStore destStore) {
637-
Long zoneId = destStore.getScope().getScopeId();
638-
List<DataStore> storesInZone = _storeMgr.getImageStoresByZoneIds(zoneId);
639-
for (DataStore sourceStore : storesInZone) {
640-
Map<String, TemplateProp> existingTemplatesInSourceStore = listTemplate(sourceStore);
641-
if (existingTemplatesInSourceStore == null || !existingTemplatesInSourceStore.containsKey(tmplt.getUniqueName())) {
642-
logger.debug("Template [{}] does not exist on image store [{}]; searching on another one.",
643-
tmplt.getUniqueName(), sourceStore.getName());
648+
if (searchAndCopyWithinZone(tmplt, destStore)) {
649+
return true;
650+
}
651+
652+
Long destZoneId = destStore.getScope().getScopeId();
653+
logger.debug("Template [{}] not found in any image store of zone [{}]. Checking other zones.",
654+
tmplt.getUniqueName(), destZoneId);
655+
656+
return searchAndCopyAcrossZones(tmplt, destStore, destZoneId);
657+
}
658+
659+
private boolean searchAndCopyAcrossZones(VMTemplateVO tmplt, DataStore destStore, Long destZoneId) {
660+
List<Long> allZoneIds = _dcDao.listAllIds();
661+
for (Long otherZoneId : allZoneIds) {
662+
if (otherZoneId.equals(destZoneId)) {
644663
continue;
645664
}
646-
TemplateObject sourceTmpl = (TemplateObject) _templateFactory.getTemplate(tmplt.getId(), sourceStore);
647-
if (sourceTmpl.getInstallPath() == null) {
648-
logger.warn("Can not copy template [{}] from image store [{}], as it returned a null install path.", tmplt.getUniqueName(),
649-
sourceStore.getName());
665+
666+
List<DataStore> storesInOtherZone = _storeMgr.getImageStoresByZoneIds(otherZoneId);
667+
logger.debug("Checking zone [{}] for template [{}]...", otherZoneId, tmplt.getUniqueName());
668+
669+
if (CollectionUtils.isEmpty(storesInOtherZone)) {
670+
logger.debug("Zone [{}] has no image stores. Skipping.", otherZoneId);
650671
continue;
651672
}
652-
storageOrchestrator.orchestrateTemplateCopyToImageStore(sourceTmpl, destStore);
653-
return true;
673+
674+
TemplateObject sourceTmpl = findUsableTemplate(tmplt, storesInOtherZone);
675+
if (sourceTmpl == null) {
676+
logger.debug("Template [{}] not found with a valid install path in any image store of zone [{}].",
677+
tmplt.getUniqueName(), otherZoneId);
678+
continue;
679+
}
680+
681+
logger.info("Template [{}] found in zone [{}]. Initiating cross-zone copy to zone [{}].",
682+
tmplt.getUniqueName(), otherZoneId, destZoneId);
683+
684+
return copyTemplateAcrossZones(destStore, sourceTmpl);
654685
}
655-
logger.debug("Can't copy template [{}] from another image store.", tmplt.getUniqueName());
686+
687+
logger.debug("Template [{}] was not found in any zone. Cannot perform zone-to-zone copy.", tmplt.getUniqueName());
656688
return false;
657689
}
658690

691+
protected TemplateObject findUsableTemplate(VMTemplateVO tmplt, List<DataStore> imageStores) {
692+
for (DataStore store : imageStores) {
693+
694+
Map<String, TemplateProp> templates = listTemplate(store);
695+
if (templates == null || !templates.containsKey(tmplt.getUniqueName())) {
696+
continue;
697+
}
698+
699+
TemplateObject tmpl = (TemplateObject) _templateFactory.getTemplate(tmplt.getId(), store);
700+
if (tmpl.getInstallPath() == null) {
701+
logger.debug("Template [{}] found in image store [{}] but install path is null. Skipping.",
702+
tmplt.getUniqueName(), store.getName());
703+
continue;
704+
}
705+
return tmpl;
706+
}
707+
return null;
708+
}
709+
710+
private boolean searchAndCopyWithinZone(VMTemplateVO tmplt, DataStore destStore) {
711+
Long destZoneId = destStore.getScope().getScopeId();
712+
List<DataStore> storesInSameZone = _storeMgr.getImageStoresByZoneIds(destZoneId);
713+
714+
TemplateObject sourceTmpl = findUsableTemplate(tmplt, storesInSameZone);
715+
if (sourceTmpl == null) {
716+
return false;
717+
}
718+
719+
TemplateApiResult result;
720+
AsyncCallFuture<TemplateApiResult> future = copyTemplateToImageStore(sourceTmpl, destStore);
721+
try {
722+
result = future.get();
723+
} catch (ExecutionException | InterruptedException e) {
724+
logger.warn("Exception while copying template [{}] from image store [{}] to image store [{}]: {}",
725+
sourceTmpl.getUniqueName(), sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
726+
result = new TemplateApiResult(sourceTmpl);
727+
result.setResult(e.getMessage());
728+
}
729+
return result.isSuccess();
730+
}
731+
732+
private boolean copyTemplateAcrossZones(DataStore destStore, TemplateObject sourceTmpl) {
733+
Long dstZoneId = destStore.getScope().getScopeId();
734+
DataCenterVO dstZone = _dcDao.findById(dstZoneId);
735+
736+
if (dstZone == null) {
737+
logger.warn("Destination zone [{}] not found for template [{}].", dstZoneId, sourceTmpl.getUniqueName());
738+
return false;
739+
}
740+
741+
TemplateApiResult result;
742+
try {
743+
VMTemplateVO template = _templateDao.findById(sourceTmpl.getId());
744+
try {
745+
DataStore sourceStore = sourceTmpl.getDataStore();
746+
long userId = CallContext.current().getCallingUserId();
747+
boolean success = _tmpltMgr.copy(userId, template, sourceStore, dstZone);
748+
749+
result = new TemplateApiResult(sourceTmpl);
750+
if (!success) {
751+
result.setResult("Cross-zone template copy failed");
752+
}
753+
} catch (StorageUnavailableException | ResourceAllocationException e) {
754+
logger.error("Exception while copying template [{}] from zone [{}] to zone [{}]",
755+
template,
756+
sourceTmpl.getDataStore().getScope().getScopeId(),
757+
dstZone.getId(),
758+
e);
759+
result = new TemplateApiResult(sourceTmpl);
760+
result.setResult(e.getMessage());
761+
} finally {
762+
ThreadContext.clearAll();
763+
}
764+
} catch (Exception e) {
765+
logger.error("Failed to copy template [{}] from zone [{}] to zone [{}].",
766+
sourceTmpl.getUniqueName(),
767+
sourceTmpl.getDataStore().getScope().getScopeId(),
768+
dstZoneId,
769+
e);
770+
return false;
771+
}
772+
773+
return result.isSuccess();
774+
}
775+
659776
@Override
660777
public AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject source, DataStore destStore) {
661778
TemplateObject sourceTmpl = (TemplateObject) source;
@@ -699,10 +816,6 @@ protected Void copyTemplateToImageStoreCallback(AsyncCallbackDispatcher<Template
699816
return null;
700817
}
701818

702-
protected boolean isCopyFromOtherStoragesEnabled(Long zoneId) {
703-
return StorageManager.COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES.valueIn(zoneId);
704-
}
705-
706819
protected void publishTemplateCreation(TemplateInfo tmplt) {
707820
VMTemplateVO tmpltVo = _templateDao.findById(tmplt.getId());
708821

0 commit comments

Comments
 (0)