This is an automated email from the ASF dual-hosted git repository.
harikrishna pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new 6932cacabc1 Allow copy of templates from secondary storages of other
zone when adding a new secondary storage (#12296)
6932cacabc1 is described below
commit 6932cacabc187cf3d76e53c7979ed10067aff2f2
Author: Harikrishna <[email protected]>
AuthorDate: Wed Jan 28 16:00:30 2026 +0530
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
---
.../command/admin/host/AddSecondaryStorageCmd.java | 24 ++-
.../service/StorageOrchestrationService.java | 3 +-
.../subsystem/api/storage/TemplateService.java | 4 +-
.../java/com/cloud/storage/StorageManager.java | 5 +-
.../engine/orchestration/StorageOrchestrator.java | 45 ++++--
.../storage/image/TemplateServiceImpl.java | 157 ++++++++++++++++---
.../storage/image/TemplateServiceImplTest.java | 171 ++++++++++++++++++++-
.../com/cloud/storage/ImageStoreDetailsUtil.java | 11 ++
.../java/com/cloud/storage/StorageManagerImpl.java | 2 +-
.../com/cloud/template/TemplateManagerImpl.java | 14 +-
ui/public/locales/en.json | 4 +-
ui/src/views/infra/AddSecondaryStorage.vue | 82 +++++++++-
ui/src/views/infra/zone/ZoneWizardAddResources.vue | 25 ++-
ui/src/views/infra/zone/ZoneWizardLaunchZone.vue | 5 +
14 files changed, 487 insertions(+), 65 deletions(-)
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
index 9a7eff7e2e5..585fd1b87a8 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
@@ -29,6 +29,11 @@ import org.apache.cloudstack.api.response.ZoneResponse;
import com.cloud.exception.DiscoveryException;
import com.cloud.storage.ImageStore;
import com.cloud.user.Account;
+import org.apache.commons.collections.MapUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
@APICommand(name = "addSecondaryStorage", description = "Adds secondary
storage.", responseObject = ImageStoreResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@@ -44,6 +49,9 @@ public class AddSecondaryStorageCmd extends BaseCmd {
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID,
entityType = ZoneResponse.class, description = "The Zone ID for the secondary
storage")
protected Long zoneId;
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Details in key/value pairs using format
details[i].keyname=keyvalue. Example:
details[0].copytemplatesfromothersecondarystorages=true")
+ protected Map details;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -56,6 +64,20 @@ public class AddSecondaryStorageCmd extends BaseCmd {
return zoneId;
}
+ public Map<String, String> getDetails() {
+ Map<String, String> detailsMap = new HashMap<>();
+ if (MapUtils.isNotEmpty(details)) {
+ Collection<?> props = details.values();
+ for (Object prop : props) {
+ HashMap<String, String> detail = (HashMap<String, String>)
prop;
+ for (Map.Entry<String, String> entry: detail.entrySet()) {
+ detailsMap.put(entry.getKey(),entry.getValue());
+ }
+ }
+ }
+ return detailsMap;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@@ -68,7 +90,7 @@ public class AddSecondaryStorageCmd extends BaseCmd {
@Override
public void execute(){
try{
- ImageStore result = _storageService.discoverImageStore(null,
getUrl(), "NFS", getZoneId(), null);
+ ImageStore result = _storageService.discoverImageStore(null,
getUrl(), "NFS", getZoneId(), getDetails());
ImageStoreResponse storeResponse = null;
if (result != null ) {
storeResponse =
_responseGenerator.createImageStoreResponse(result);
diff --git
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
index 8be2015bfef..4af0c806060 100644
---
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
+++
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
@@ -22,7 +22,6 @@ import java.util.concurrent.Future;
import org.apache.cloudstack.api.response.MigrationResponse;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
-import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import
org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy;
@@ -31,5 +30,5 @@ public interface StorageOrchestrationService {
MigrationResponse migrateResources(Long srcImgStoreId, Long
destImgStoreId, List<Long> templateIdList, List<Long> snapshotIdList);
- Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo
source, DataStore destStore);
+ Future<TemplateApiResult> orchestrateTemplateCopyFromSecondaryStores(long
templateId, DataStore destStore);
}
diff --git
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
index a8861d5acc6..269eb4f1c21 100644
---
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
+++
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
@@ -80,4 +80,6 @@ public interface TemplateService {
List<DatadiskTO> getTemplateDatadisksOnImageStore(TemplateInfo
templateInfo, String configurationId);
AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject
source, DataStore destStore);
-}
+
+ void handleTemplateCopyFromSecondaryStores(long templateId, DataStore
destStore);
+ }
diff --git
a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java
b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java
index de0cb34d63e..4ce1f4a9638 100644
--- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java
+++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java
@@ -220,8 +220,9 @@ public interface StorageManager extends StorageService {
"storage.pool.host.connect.workers", "1",
"Number of worker threads to be used to connect hosts to a primary
storage", true);
- ConfigKey<Boolean> COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES = new
ConfigKey<>(Boolean.class, "copy.public.templates.from.other.storages",
- "Storage", "true", "Allow SSVMs to try copying public templates
from one secondary storage to another instead of downloading them from the
source.",
+ ConfigKey<Boolean> COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES = new
ConfigKey<>(Boolean.class, "copy.templates.from.other.secondary.storages",
+ "Storage", "true", "When enabled, this feature allows templates to
be copied from existing Secondary Storage servers (within the same zone or
across zones) " +
+ "while adding a new Secondary Storage. If the copy operation
fails, the system falls back to downloading the template from the source URL.",
true, ConfigKey.Scope.Zone, null);
/**
diff --git
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
index 37a1f8dc196..933b4e0c5ce 100644
---
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
+++
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
@@ -36,6 +36,9 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.template.TemplateManager;
import org.apache.cloudstack.api.response.MigrationResponse;
import
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
@@ -45,6 +48,7 @@ import
org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageServic
import
org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageService.DataObjectResult;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService;
import
org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
@@ -103,6 +107,15 @@ public class StorageOrchestrator extends ManagerBase
implements StorageOrchestra
VolumeDataStoreDao volumeDataStoreDao;
@Inject
DataMigrationUtility migrationHelper;
+ @Inject
+ TemplateManager templateManager;
+ @Inject
+ VMTemplateDao templateDao;
+ @Inject
+ TemplateDataFactory templateDataFactory;
+ @Inject
+ DataCenterDao dcDao;
+
ConfigKey<Double> ImageStoreImbalanceThreshold = new
ConfigKey<>("Advanced", Double.class,
"image.store.imbalance.threshold",
@@ -304,8 +317,9 @@ public class StorageOrchestrator extends ManagerBase
implements StorageOrchestra
}
@Override
- public Future<TemplateApiResult>
orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore) {
- return submit(destStore.getScope().getScopeId(), new
CopyTemplateTask(source, destStore));
+ public Future<TemplateApiResult>
orchestrateTemplateCopyFromSecondaryStores(long srcTemplateId, DataStore
destStore) {
+ Long dstZoneId = destStore.getScope().getScopeId();
+ return submit(dstZoneId, new
CopyTemplateFromSecondaryStorageTask(srcTemplateId, destStore));
}
protected Pair<String, Boolean> migrateCompleted(Long destDatastoreId,
DataStore srcDatastore, List<DataObject> files, MigrationPolicy
migrationPolicy, int skipped) {
@@ -624,13 +638,13 @@ public class StorageOrchestrator extends ManagerBase
implements StorageOrchestra
}
}
- private class CopyTemplateTask implements Callable<TemplateApiResult> {
- private TemplateInfo sourceTmpl;
- private DataStore destStore;
- private String logid;
+ private class CopyTemplateFromSecondaryStorageTask implements
Callable<TemplateApiResult> {
+ private final long srcTemplateId;
+ private final DataStore destStore;
+ private final String logid;
- public CopyTemplateTask(TemplateInfo sourceTmpl, DataStore destStore) {
- this.sourceTmpl = sourceTmpl;
+ CopyTemplateFromSecondaryStorageTask(long srcTemplateId, DataStore
destStore) {
+ this.srcTemplateId = srcTemplateId;
this.destStore = destStore;
this.logid = ThreadContext.get(LOGCONTEXTID);
}
@@ -639,17 +653,16 @@ public class StorageOrchestrator extends ManagerBase
implements StorageOrchestra
public TemplateApiResult call() {
ThreadContext.put(LOGCONTEXTID, logid);
TemplateApiResult result;
- AsyncCallFuture<TemplateApiResult> future =
templateService.copyTemplateToImageStore(sourceTmpl, destStore);
+ long destZoneId = destStore.getScope().getScopeId();
+ TemplateInfo sourceTmpl =
templateDataFactory.getTemplate(srcTemplateId, DataStoreRole.Image);
try {
- result = future.get();
- } catch (ExecutionException | InterruptedException e) {
- logger.warn("Exception while copying template [{}] from image
store [{}] to image store [{}]: {}",
- sourceTmpl.getUniqueName(),
sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
+
templateService.handleTemplateCopyFromSecondaryStores(srcTemplateId, destStore);
result = new TemplateApiResult(sourceTmpl);
- result.setResult(e.getMessage());
+ } finally {
+ tryCleaningUpExecutor(destZoneId);
+ ThreadContext.clearAll();
}
- tryCleaningUpExecutor(destStore.getScope().getScopeId());
- ThreadContext.clearAll();
+
return result;
}
}
diff --git
a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
index bee62955051..5fc9bbac352 100644
---
a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
+++
b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
@@ -31,6 +31,8 @@ import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
+import com.cloud.exception.StorageUnavailableException;
+import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
@@ -67,9 +69,11 @@ import
org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
import org.apache.cloudstack.storage.image.store.TemplateObject;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
import org.springframework.stereotype.Component;
import com.cloud.agent.api.Answer;
@@ -567,10 +571,7 @@ public class TemplateServiceImpl implements
TemplateService {
}
if
(availHypers.contains(tmplt.getHypervisorType())) {
- boolean copied =
isCopyFromOtherStoragesEnabled(zoneId) && tryCopyingTemplateToImageStore(tmplt,
store);
- if (!copied) {
- tryDownloadingTemplateToImageStore(tmplt,
store);
- }
+
storageOrchestrator.orchestrateTemplateCopyFromSecondaryStores(tmplt.getId(),
store);
} else {
logger.info("Skip downloading template {}
since current data center does not have hypervisor {}", tmplt,
tmplt.getHypervisorType());
}
@@ -617,6 +618,16 @@ public class TemplateServiceImpl implements
TemplateService {
}
+ @Override
+ public void handleTemplateCopyFromSecondaryStores(long templateId,
DataStore destStore) {
+ VMTemplateVO template = _templateDao.findById(templateId);
+ long zoneId = destStore.getScope().getScopeId();
+ boolean copied =
imageStoreDetailsUtil.isCopyTemplatesFromOtherStoragesEnabled(destStore.getId(),
zoneId) && tryCopyingTemplateToImageStore(template, destStore);
+ if (!copied) {
+ tryDownloadingTemplateToImageStore(template, destStore);
+ }
+ }
+
protected void tryDownloadingTemplateToImageStore(VMTemplateVO tmplt,
DataStore destStore) {
if (tmplt.getUrl() == null) {
logger.info("Not downloading template [{}] to image store [{}], as
it has no URL.", tmplt.getUniqueName(),
@@ -634,28 +645,134 @@ public class TemplateServiceImpl implements
TemplateService {
}
protected boolean tryCopyingTemplateToImageStore(VMTemplateVO tmplt,
DataStore destStore) {
- Long zoneId = destStore.getScope().getScopeId();
- List<DataStore> storesInZone =
_storeMgr.getImageStoresByZoneIds(zoneId);
- for (DataStore sourceStore : storesInZone) {
- Map<String, TemplateProp> existingTemplatesInSourceStore =
listTemplate(sourceStore);
- if (existingTemplatesInSourceStore == null ||
!existingTemplatesInSourceStore.containsKey(tmplt.getUniqueName())) {
- logger.debug("Template [{}] does not exist on image store
[{}]; searching on another one.",
- tmplt.getUniqueName(), sourceStore.getName());
+ if (searchAndCopyWithinZone(tmplt, destStore)) {
+ return true;
+ }
+
+ Long destZoneId = destStore.getScope().getScopeId();
+ logger.debug("Template [{}] not found in any image store of zone [{}].
Checking other zones.",
+ tmplt.getUniqueName(), destZoneId);
+
+ return searchAndCopyAcrossZones(tmplt, destStore, destZoneId);
+ }
+
+ private boolean searchAndCopyAcrossZones(VMTemplateVO tmplt, DataStore
destStore, Long destZoneId) {
+ List<Long> allZoneIds = _dcDao.listAllIds();
+ for (Long otherZoneId : allZoneIds) {
+ if (otherZoneId.equals(destZoneId)) {
continue;
}
- TemplateObject sourceTmpl = (TemplateObject)
_templateFactory.getTemplate(tmplt.getId(), sourceStore);
- if (sourceTmpl.getInstallPath() == null) {
- logger.warn("Can not copy template [{}] from image store [{}],
as it returned a null install path.", tmplt.getUniqueName(),
- sourceStore.getName());
+
+ List<DataStore> storesInOtherZone =
_storeMgr.getImageStoresByZoneIds(otherZoneId);
+ logger.debug("Checking zone [{}] for template [{}]...",
otherZoneId, tmplt.getUniqueName());
+
+ if (CollectionUtils.isEmpty(storesInOtherZone)) {
+ logger.debug("Zone [{}] has no image stores. Skipping.",
otherZoneId);
continue;
}
-
storageOrchestrator.orchestrateTemplateCopyToImageStore(sourceTmpl, destStore);
- return true;
+
+ TemplateObject sourceTmpl = findUsableTemplate(tmplt,
storesInOtherZone);
+ if (sourceTmpl == null) {
+ logger.debug("Template [{}] not found with a valid install
path in any image store of zone [{}].",
+ tmplt.getUniqueName(), otherZoneId);
+ continue;
+ }
+
+ logger.info("Template [{}] found in zone [{}]. Initiating
cross-zone copy to zone [{}].",
+ tmplt.getUniqueName(), otherZoneId, destZoneId);
+
+ return copyTemplateAcrossZones(destStore, sourceTmpl);
}
- logger.debug("Can't copy template [{}] from another image store.",
tmplt.getUniqueName());
+
+ logger.debug("Template [{}] was not found in any zone. Cannot perform
zone-to-zone copy.", tmplt.getUniqueName());
return false;
}
+ protected TemplateObject findUsableTemplate(VMTemplateVO tmplt,
List<DataStore> imageStores) {
+ for (DataStore store : imageStores) {
+
+ Map<String, TemplateProp> templates = listTemplate(store);
+ if (templates == null ||
!templates.containsKey(tmplt.getUniqueName())) {
+ continue;
+ }
+
+ TemplateObject tmpl = (TemplateObject)
_templateFactory.getTemplate(tmplt.getId(), store);
+ if (tmpl.getInstallPath() == null) {
+ logger.debug("Template [{}] found in image store [{}] but
install path is null. Skipping.",
+ tmplt.getUniqueName(), store.getName());
+ continue;
+ }
+ return tmpl;
+ }
+ return null;
+ }
+
+ private boolean searchAndCopyWithinZone(VMTemplateVO tmplt, DataStore
destStore) {
+ Long destZoneId = destStore.getScope().getScopeId();
+ List<DataStore> storesInSameZone =
_storeMgr.getImageStoresByZoneIds(destZoneId);
+
+ TemplateObject sourceTmpl = findUsableTemplate(tmplt,
storesInSameZone);
+ if (sourceTmpl == null) {
+ return false;
+ }
+
+ TemplateApiResult result;
+ AsyncCallFuture<TemplateApiResult> future =
copyTemplateToImageStore(sourceTmpl, destStore);
+ try {
+ result = future.get();
+ } catch (ExecutionException | InterruptedException e) {
+ logger.warn("Exception while copying template [{}] from image
store [{}] to image store [{}]: {}",
+ sourceTmpl.getUniqueName(),
sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
+ result = new TemplateApiResult(sourceTmpl);
+ result.setResult(e.getMessage());
+ }
+ return result.isSuccess();
+ }
+
+ private boolean copyTemplateAcrossZones(DataStore destStore,
TemplateObject sourceTmpl) {
+ Long dstZoneId = destStore.getScope().getScopeId();
+ DataCenterVO dstZone = _dcDao.findById(dstZoneId);
+
+ if (dstZone == null) {
+ logger.warn("Destination zone [{}] not found for template [{}].",
dstZoneId, sourceTmpl.getUniqueName());
+ return false;
+ }
+
+ TemplateApiResult result;
+ try {
+ VMTemplateVO template = _templateDao.findById(sourceTmpl.getId());
+ try {
+ DataStore sourceStore = sourceTmpl.getDataStore();
+ long userId = CallContext.current().getCallingUserId();
+ boolean success = _tmpltMgr.copy(userId, template,
sourceStore, dstZone);
+
+ result = new TemplateApiResult(sourceTmpl);
+ if (!success) {
+ result.setResult("Cross-zone template copy failed");
+ }
+ } catch (StorageUnavailableException | ResourceAllocationException
e) {
+ logger.error("Exception while copying template [{}] from zone
[{}] to zone [{}]",
+ template,
+ sourceTmpl.getDataStore().getScope().getScopeId(),
+ dstZone.getId(),
+ e);
+ result = new TemplateApiResult(sourceTmpl);
+ result.setResult(e.getMessage());
+ } finally {
+ ThreadContext.clearAll();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to copy template [{}] from zone [{}] to zone
[{}].",
+ sourceTmpl.getUniqueName(),
+ sourceTmpl.getDataStore().getScope().getScopeId(),
+ dstZoneId,
+ e);
+ return false;
+ }
+
+ return result.isSuccess();
+ }
+
@Override
public AsyncCallFuture<TemplateApiResult>
copyTemplateToImageStore(DataObject source, DataStore destStore) {
TemplateObject sourceTmpl = (TemplateObject) source;
@@ -699,10 +816,6 @@ public class TemplateServiceImpl implements
TemplateService {
return null;
}
- protected boolean isCopyFromOtherStoragesEnabled(Long zoneId) {
- return
StorageManager.COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES.valueIn(zoneId);
- }
-
protected void publishTemplateCreation(TemplateInfo tmplt) {
VMTemplateVO tmpltVo = _templateDao.findById(tmplt.getId());
diff --git
a/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
b/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
index cb7994915b3..e9eac045869 100644
---
a/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
+++
b/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
@@ -18,13 +18,20 @@
*/
package org.apache.cloudstack.storage.image;
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.StorageUnavailableException;
+import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.template.TemplateProp;
import com.cloud.template.TemplateManager;
+import com.cloud.user.Account;
+import com.cloud.user.User;
+import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
-import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.storage.image.store.TemplateObject;
@@ -46,6 +53,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import static org.mockito.Mockito.mock;
+
@RunWith(MockitoJUnitRunner.class)
public class TemplateServiceImplTest {
@@ -89,6 +98,12 @@ public class TemplateServiceImplTest {
@Mock
TemplateManager templateManagerMock;
+ @Mock
+ VMTemplateDao templateDao;
+
+ @Mock
+ DataCenterDao _dcDao;
+
Map<String, TemplateProp> templatesInSourceStore = new HashMap<>();
@Before
@@ -101,7 +116,6 @@ public class TemplateServiceImplTest {
Mockito.doReturn(List.of(sourceStoreMock,
destStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(zoneId);
Mockito.doReturn(templatesInSourceStore).when(templateService).listTemplate(sourceStoreMock);
Mockito.doReturn(null).when(templateService).listTemplate(destStoreMock);
-
Mockito.doReturn("install-path").when(templateInfoMock).getInstallPath();
Mockito.doReturn(templateInfoMock).when(templateDataFactoryMock).getTemplate(2L,
sourceStoreMock);
Mockito.doReturn(3L).when(dataStoreMock).getId();
Mockito.doReturn(zoneScopeMock).when(dataStoreMock).getScope();
@@ -166,7 +180,7 @@ public class TemplateServiceImplTest {
boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
Assert.assertFalse(result);
- Mockito.verify(storageOrchestrator,
Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(),
Mockito.any());
+ Mockito.verify(storageOrchestrator,
Mockito.never()).orchestrateTemplateCopyFromSecondaryStores(Mockito.anyLong(),
Mockito.any());
}
@Test
@@ -174,20 +188,161 @@ public class TemplateServiceImplTest {
templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
Mockito.doReturn(null).when(templateInfoMock).getInstallPath();
+ Scope scopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+ Mockito.doReturn(1L).when(scopeMock).getScopeId();
+ Mockito.doReturn(List.of(1L)).when(_dcDao).listAllIds();
+
boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
Assert.assertFalse(result);
- Mockito.verify(storageOrchestrator,
Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(),
Mockito.any());
+ Mockito.verify(storageOrchestrator,
Mockito.never()).orchestrateTemplateCopyFromSecondaryStores(Mockito.anyLong(),
Mockito.any());
}
@Test
- public void
tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherStorageAndTaskWasScheduled()
{
- templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
- Mockito.doReturn(new
AsyncCallFuture<>()).when(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(),
Mockito.any());
+ public void
tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherZone()
throws StorageUnavailableException, ResourceAllocationException {
+ Scope scopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+ Mockito.doReturn(1L).when(scopeMock).getScopeId();
+ Mockito.doReturn(100L).when(tmpltMock).getId();
+ Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+
Mockito.doReturn(null).when(templateService).listTemplate(sourceStoreMock);
+ Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+
+ DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+ Map<String, TemplateProp> templatesInOtherZone = new HashMap<>();
+ templatesInOtherZone.put("unique-name", tmpltPropMock);
+
Mockito.doReturn(templatesInOtherZone).when(templateService).listTemplate(otherZoneStoreMock);
+
+ TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
otherZoneStoreMock);
+
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+
+ DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
+ Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
+
Mockito.doReturn(true).when(templateManagerMock).copy(Mockito.anyLong(),
Mockito.any(), Mockito.any(), Mockito.any());
boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
Assert.assertTrue(result);
-
Mockito.verify(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(),
Mockito.any());
+ }
+
+ @Test
+ public void
tryCopyingTemplateToImageStoreTestReturnsFalseWhenDestinationZoneIsMissing() {
+ Scope scopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+ Mockito.doReturn(1L).when(scopeMock).getScopeId();
+ Mockito.doReturn(100L).when(tmpltMock).getId();
+ Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+ Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+
+ DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+ Map<String, TemplateProp> templates = new HashMap<>();
+ templates.put("unique-name", tmpltPropMock);
+
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);
+
+ TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
otherZoneStoreMock);
+
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+ Mockito.doReturn(null).when(_dcDao).findById(1L);
+
+ boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+ Assert.assertFalse(result);
+ }
+
+ @Test
+ public void
tryCopyingTemplateToImageStoreTestReturnsTrueWhenCrossZoneCopyTaskIsScheduled()
throws StorageUnavailableException, ResourceAllocationException {
+ Scope scopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+ Mockito.doReturn(1L).when(scopeMock).getScopeId();
+ Mockito.doReturn(100L).when(tmpltMock).getId();
+ Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+ Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+
+ DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+ Map<String, TemplateProp> templates = new HashMap<>();
+ templates.put("unique-name", tmpltPropMock);
+
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);
+
+ TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
otherZoneStoreMock);
+
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+ Mockito.doReturn(100L).when(sourceTmplMock).getId();
+
+ DataStore sourceStoreMock = Mockito.mock(DataStore.class);
+ Scope sourceScopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(sourceStoreMock).when(sourceTmplMock).getDataStore();
+
+ DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
+ Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
+ VMTemplateVO templateVoMock = Mockito.mock(VMTemplateVO.class);
+ Mockito.doReturn(templateVoMock).when(templateDao).findById(100L);
+
+
Mockito.doReturn(true).when(templateManagerMock).copy(Mockito.anyLong(),
Mockito.any(), Mockito.any(), Mockito.any());
+
+ Account account = mock(Account.class);
+ User user = mock(User.class);
+ CallContext callContext = mock(CallContext.class);
+
+ boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+ Assert.assertTrue(result);
+ }
+
+ @Test
+ public void
tryCopyingTemplateToImageStoreTestReturnsFalseWhenTemplateNotFoundInAnyZone() {
+ Scope scopeMock = Mockito.mock(Scope.class);
+ Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+ Mockito.doReturn(1L).when(scopeMock).getScopeId();
+ Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(Mockito.anyLong());
+
Mockito.doReturn(null).when(templateService).listTemplate(Mockito.any());
+
+ boolean result =
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+ Assert.assertFalse(result);
+ }
+
+ @Test
+ public void testFindUsableTemplateReturnsTemplateWithNonNullInstallPath() {
+ VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
+ Mockito.when(template.getId()).thenReturn(10L);
+ Mockito.when(template.getUniqueName()).thenReturn("test-template");
+
+ DataStore storeWithNullPath = Mockito.mock(DataStore.class);
+ Mockito.when(storeWithNullPath.getName()).thenReturn("store-null");
+
+ DataStore storeWithValidPath = Mockito.mock(DataStore.class);
+ TemplateObject tmplWithNullPath = Mockito.mock(TemplateObject.class);
+ Mockito.when(tmplWithNullPath.getInstallPath()).thenReturn(null);
+
+ TemplateObject tmplWithValidPath = Mockito.mock(TemplateObject.class);
+
Mockito.when(tmplWithValidPath.getInstallPath()).thenReturn("/mnt/secondary/template.qcow2");
+
+
Mockito.doReturn(tmplWithNullPath).when(templateDataFactoryMock).getTemplate(10L,
storeWithNullPath);
+
Mockito.doReturn(tmplWithValidPath).when(templateDataFactoryMock).getTemplate(10L,
storeWithValidPath);
+
+ Map<String, TemplateProp> templates = new HashMap<>();
+ templates.put("test-template", Mockito.mock(TemplateProp.class));
+
+
Mockito.doReturn(templates).when(templateService).listTemplate(storeWithNullPath);
+
Mockito.doReturn(templates).when(templateService).listTemplate(storeWithValidPath);
+
+ List<DataStore> imageStores = List.of(storeWithNullPath,
storeWithValidPath);
+
+ TemplateObject result = templateService.findUsableTemplate(template,
imageStores);
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(tmplWithValidPath, result);
}
}
diff --git a/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
b/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
index baf5ef8902d..9f5aa660f4f 100755
--- a/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
+++ b/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
@@ -78,4 +78,15 @@ public class ImageStoreDetailsUtil {
return getGlobalDefaultNfsVersion();
}
+ public boolean isCopyTemplatesFromOtherStoragesEnabled(Long storeId, Long
zoneId) {
+ final Map<String, String> storeDetails =
imageStoreDetailsDao.getDetails(storeId);
+ final String keyWithoutDots =
StorageManager.COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES.key()
+ .replace(".", "");
+
+ if (storeDetails != null && storeDetails.containsKey(keyWithoutDots)) {
+ return Boolean.parseBoolean(storeDetails.get(keyWithoutDots));
+ }
+
+ return
StorageManager.COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES.valueIn(zoneId);
+ }
}
diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
index 13b7fbb00c2..d1dca0fa901 100644
--- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
+++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
@@ -4206,7 +4206,7 @@ public class StorageManagerImpl extends ManagerBase
implements StorageManager, C
DataStoreDownloadFollowRedirects,
AllowVolumeReSizeBeyondAllocation,
StoragePoolHostConnectWorkers,
- COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES
+ COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES
};
}
diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
index 5773410c35a..78265021c0a 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -842,6 +842,9 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
// Copy will just find one eligible image store for the destination
zone
// and copy template there, not propagate to all image stores
// for that zone
+
+ boolean copied = false;
+
for (DataStore dstSecStore : dstSecStores) {
TemplateDataStoreVO dstTmpltStore =
_tmplStoreDao.findByStoreTemplate(dstSecStore.getId(), tmpltId);
if (dstTmpltStore != null && dstTmpltStore.getDownloadState() ==
Status.DOWNLOADED) {
@@ -856,9 +859,12 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
TemplateApiResult result = future.get();
if (result.isFailed()) {
logger.debug("Copy Template failed for image store {}:
{}", dstSecStore, result.getResult());
+ _tmplStoreDao.removeByTemplateStore(tmpltId,
dstSecStore.getId());
continue; // try next image store
}
+ copied = true;
+
_tmpltDao.addTemplateToZone(template, dstZoneId);
if (account.getId() != Account.ACCOUNT_ID_SYSTEM) {
@@ -886,12 +892,14 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
}
}
}
+
+ return true;
+
} catch (Exception ex) {
- logger.debug("Failed to copy Template to image store:{} ,will
try next one", dstSecStore);
+ logger.debug("Failed to copy Template to image store:{} ,will
try next one", dstSecStore, ex);
}
}
- return true;
-
+ return copied;
}
@Override
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index b2465fa325f..99873820d53 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -591,6 +591,8 @@
"label.copy.consoleurl": "Copy console URL to clipboard",
"label.copyid": "Copy ID",
"label.copy.password": "Copy password",
+"label.copy.templates.from.other.secondary.storages": "Copy Templates from
other storages instead of fetching from URLs",
+"label.copy.templates.from.other.secondary.storages.add.zone": "Copy Templates
from other storages",
"label.core": "Core",
"label.core.zone.type": "Core Zone type",
"label.counter": "Counter",
@@ -3019,7 +3021,7 @@
"message.desc.importmigratefromvmwarewizard": "By selecting an existing or
external VMware Datacenter and an instance to import, CloudStack migrates the
selected instance from VMware to KVM on a conversion host using virt-v2v and
imports it into a KVM Cluster",
"message.desc.primary.storage": "Each Cluster must contain one or more primary
storage servers. We will add the first one now. Primary storage contains the
disk volumes for all the Instances running on hosts in the cluster. Use any
standards-compliant protocol that is supported by the underlying hypervisor.",
"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you
would like to add to this Instance.",
-"message.desc.secondary.storage": "Each Zone must have at least one NFS or
secondary storage server. We will add the first one now. Secondary storage
stores Instance Templates, ISO images, and Instance disk volume Snapshots. This
server must be available to all hosts in the zone.<br/><br/>Provide the IP
address and exported path.",
+"message.desc.secondary.storage": "Each Zone must have at least one NFS or
secondary storage server. We will add the first one now. Secondary storage
stores Instance Templates, ISO images, and Instance disk volume Snapshots. This
server must be available to all hosts in the zone.<br/><br/>Provide the IP
address and exported path.<br/><br/> \"Copy templates from other secondary
storages\" switch can be used to automatically copy existing templates from
secondary storages in other zones in [...]
"message.desc.register.user.data": "Please fill in the following to register
new User Data.",
"message.desc.registered.user.data": "Registered a User Data.",
"message.desc.zone": "A Zone is the largest organizational unit in CloudStack,
and it typically corresponds to a single datacenter. Zones provide physical
isolation and redundancy. A zone consists of one or more Pods (each of which
contains hosts and primary storage servers) and a secondary storage server
which is shared by all pods in the zone.",
diff --git a/ui/src/views/infra/AddSecondaryStorage.vue
b/ui/src/views/infra/AddSecondaryStorage.vue
index 746af5b959d..db4893115a6 100644
--- a/ui/src/views/infra/AddSecondaryStorage.vue
+++ b/ui/src/views/infra/AddSecondaryStorage.vue
@@ -48,6 +48,7 @@
<a-form-item name="zone" ref="zone" :label="$t('label.zone')">
<a-select
v-model:value="form.zone"
+ @change="onZoneChange"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
@@ -105,6 +106,7 @@
<a-form-item name="zone" ref="zone" :label="$t('label.zone')">
<a-select
v-model:value="form.zone"
+ @change="onZoneChange"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
@@ -159,6 +161,17 @@
<a-input v-model:value="form.secondaryStorageNFSPath"/>
</a-form-item>
</div>
+ <div v-if="showCopyTemplatesToggle">
+ <a-form-item
+ name="copyTemplatesFromOtherSecondaryStorages"
+ ref="copyTemplatesFromOtherSecondaryStorages"
+ :label="$t('label.copy.templates.from.other.secondary.storages')">
+ <a-switch
+ v-model:checked="form.copyTemplatesFromOtherSecondaryStorages"
+ @change="onCopyTemplatesToggleChanged"
+ />
+ </a-form-item>
+ </div>
<div :span="24" class="action-button">
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" ref="submit" @click="handleSubmit">{{
$t('label.ok') }}</a-button>
@@ -191,7 +204,9 @@ export default {
providers: ['NFS', 'SMB/CIFS', 'S3', 'Swift'],
zones: [],
loading: false,
- secondaryStorageNFSStaging: false
+ secondaryStorageNFSStaging: false,
+ showCopyTemplatesToggle: false,
+ copyTemplatesTouched: false
}
},
created () {
@@ -203,7 +218,8 @@ export default {
this.formRef = ref()
this.form = reactive({
provider: 'NFS',
- secondaryStorageHttps: true
+ secondaryStorageHttps: true,
+ copyTemplatesFromOtherSecondaryStorages: true
})
this.rules = reactive({
zone: [{ required: true, message: this.$t('label.required') }],
@@ -225,20 +241,56 @@ export default {
},
fetchData () {
this.listZones()
+ this.checkOtherSecondaryStorages()
},
closeModal () {
this.$emit('close-action')
},
+ fetchCopyTemplatesConfig () {
+ if (!this.form.zone) {
+ return
+ }
+
+ api('listConfigurations', {
+ name: 'copy.templates.from.other.secondary.storages',
+ zoneid: this.form.zone
+ }).then(json => {
+ const items =
+ json?.listconfigurationsresponse?.configuration || []
+
+ items.forEach(item => {
+ if (item.name === 'copy.templates.from.other.secondary.storages') {
+ this.form.copyTemplatesFromOtherSecondaryStorages =
+ item.value === 'true'
+ }
+ })
+ })
+ },
+ onZoneChange (val) {
+ this.form.zone = val
+ this.copyTemplatesTouched = false
+ this.fetchCopyTemplatesConfig()
+ },
listZones () {
api('listZones', { showicon: true }).then(json => {
- if (json && json.listzonesresponse && json.listzonesresponse.zone) {
- this.zones = json.listzonesresponse.zone
- if (this.zones.length > 0) {
- this.form.zone = this.zones[0].id || ''
- }
+ this.zones = json.listzonesresponse.zone || []
+
+ if (this.zones.length > 0) {
+ this.form.zone = this.zones[0].id
+ this.fetchCopyTemplatesConfig()
}
})
},
+ checkOtherSecondaryStorages () {
+ api('listImageStores', { listall: true }).then(json => {
+ const stores = json?.listimagestoresresponse?.imagestore || []
+
+ this.showCopyTemplatesToggle = stores.length > 0
+ })
+ },
+ onCopyTemplatesToggleChanged (val) {
+ this.copyTemplatesTouched = true
+ },
nfsURL (server, path) {
var url
if (path.substring(0, 1) !== '/') {
@@ -362,6 +414,22 @@ export default {
nfsParams.url = nfsUrl
}
+ if (
+ this.showCopyTemplatesToggle &&
+ this.copyTemplatesTouched
+ ) {
+ const copyTemplatesKey = 'copytemplatesfromothersecondarystorages'
+
+ const detailIdx = Object.keys(data)
+ .filter(k => k.startsWith('details['))
+ .map(k => parseInt(k.match(/details\[(\d+)\]/)[1]))
+ .reduce((a, b) => Math.max(a, b), -1) + 1
+
+ data[`details[${detailIdx}].key`] = copyTemplatesKey
+ data[`details[${detailIdx}].value`] =
+ values.copyTemplatesFromOtherSecondaryStorages.toString()
+ }
+
this.loading = true
try {
diff --git a/ui/src/views/infra/zone/ZoneWizardAddResources.vue
b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
index 4bd602f0aca..298cc7fec9d 100644
--- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue
+++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
@@ -840,6 +840,13 @@ export default {
display: {
secondaryStorageProvider: ['Swift']
}
+ },
+ {
+ title: 'label.copy.templates.from.other.secondary.storages.add.zone',
+ key: 'copyTemplatesFromOtherSecondaryStorages',
+ required: false,
+ switch: true,
+ checked: this.copytemplate
}
]
}
@@ -860,7 +867,8 @@ export default {
}],
storageProviders: [],
currentStep: null,
- options: ['primaryStorageScope', 'primaryStorageProtocol', 'provider',
'primaryStorageProvider']
+ options: ['primaryStorageScope', 'primaryStorageProtocol', 'provider',
'primaryStorageProvider'],
+ copytemplate: true
}
},
created () {
@@ -885,6 +893,7 @@ export default {
primaryStorageScope: null
})
}
+
this.applyCopyTemplatesOptionFromGlobalSettingDuringSecondaryStorageAddition()
}
},
watch: {
@@ -1108,6 +1117,20 @@ export default {
this.storageProviders = storageProviders
})
},
+ applyCopyTemplatesOptionFromGlobalSettingDuringSecondaryStorageAddition ()
{
+ api('listConfigurations', {
+ name: 'copy.templates.from.other.secondary.storages'
+ }).then(json => {
+ const config = json?.listconfigurationsresponse?.configuration?.[0]
+
+ if (!config || config.value === undefined) {
+ return
+ }
+
+ const value = String(config.value).toLowerCase() === 'true'
+ this.copytemplate = value
+ })
+ },
fetchPrimaryStorageProvider () {
this.primaryStorageProviders = []
api('listStorageProviders', { type: 'primary' }).then(json => {
diff --git a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
index a787ad839cd..fbf5e6f5c20 100644
--- a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
+++ b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
@@ -1580,6 +1580,11 @@ export default {
params.provider = this.prefillContent.secondaryStorageProvider
params.zoneid = this.stepData.zoneReturned.id
params.url = url
+ if (this.prefillContent.copyTemplatesFromOtherSecondaryStorages !==
undefined) {
+ params['details[0].key'] = 'copytemplatesfromothersecondarystorages'
+ params['details[0].value'] =
+ this.prefillContent.copyTemplatesFromOtherSecondaryStorages
+ }
} else if (this.prefillContent.secondaryStorageProvider === 'SMB') {
const nfsServer = this.prefillContent.secondaryStorageServer
const path = this.prefillContent.secondaryStoragePath