mirror of https://github.com/apache/cloudstack.git
Prioritize copying templates from other secondary storages instead of downloading them (#10363)
* Prioritize copying templates from other secondary storages instead of downloading them * Treat some corner cases
This commit is contained in:
parent
ba26d95ad7
commit
e8200a0b74
|
|
@ -18,12 +18,18 @@
|
||||||
package org.apache.cloudstack.engine.orchestration.service;
|
package org.apache.cloudstack.engine.orchestration.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import org.apache.cloudstack.api.response.MigrationResponse;
|
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;
|
import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy;
|
||||||
|
|
||||||
public interface StorageOrchestrationService {
|
public interface StorageOrchestrationService {
|
||||||
MigrationResponse migrateData(Long srcDataStoreId, List<Long> destDatastores, MigrationPolicy migrationPolicy);
|
MigrationResponse migrateData(Long srcDataStoreId, List<Long> destDatastores, MigrationPolicy migrationPolicy);
|
||||||
|
|
||||||
MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreId, List<Long> templateIdList, List<Long> snapshotIdList);
|
MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreId, List<Long> templateIdList, List<Long> snapshotIdList);
|
||||||
|
|
||||||
|
Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,6 @@ public interface TemplateService {
|
||||||
AsyncCallFuture<TemplateApiResult> createDatadiskTemplateAsync(TemplateInfo parentTemplate, TemplateInfo dataDiskTemplate, String path, String diskId, long fileSize, boolean bootable);
|
AsyncCallFuture<TemplateApiResult> createDatadiskTemplateAsync(TemplateInfo parentTemplate, TemplateInfo dataDiskTemplate, String path, String diskId, long fileSize, boolean bootable);
|
||||||
|
|
||||||
List<DatadiskTO> getTemplateDatadisksOnImageStore(TemplateInfo templateInfo, String configurationId);
|
List<DatadiskTO> getTemplateDatadisksOnImageStore(TemplateInfo templateInfo, String configurationId);
|
||||||
|
|
||||||
|
AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject source, DataStore destStore);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,10 @@ public interface StorageManager extends StorageService {
|
||||||
"storage.pool.host.connect.workers", "1",
|
"storage.pool.host.connect.workers", "1",
|
||||||
"Number of worker threads to be used to connect hosts to a primary storage", true);
|
"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.",
|
||||||
|
true, ConfigKey.Scope.Zone, null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* should we execute in sequence not involving any storages?
|
* should we execute in sequence not involving any storages?
|
||||||
* @return tru if commands should execute in sequence
|
* @return tru if commands should execute in sequence
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
|
@ -206,12 +208,22 @@ public class DataMigrationUtility {
|
||||||
|
|
||||||
protected List<DataObject> getAllReadyTemplates(DataStore srcDataStore, Map<DataObject, Pair<List<TemplateInfo>, Long>> childTemplates, List<TemplateDataStoreVO> templates) {
|
protected List<DataObject> getAllReadyTemplates(DataStore srcDataStore, Map<DataObject, Pair<List<TemplateInfo>, Long>> childTemplates, List<TemplateDataStoreVO> templates) {
|
||||||
List<TemplateInfo> files = new LinkedList<>();
|
List<TemplateInfo> files = new LinkedList<>();
|
||||||
|
Set<Long> idsForMigration = new HashSet<>();
|
||||||
|
|
||||||
for (TemplateDataStoreVO template : templates) {
|
for (TemplateDataStoreVO template : templates) {
|
||||||
VMTemplateVO templateVO = templateDao.findById(template.getTemplateId());
|
long templateId = template.getTemplateId();
|
||||||
if (shouldMigrateTemplate(template, templateVO)) {
|
if (idsForMigration.contains(templateId)) {
|
||||||
|
logger.warn("Template store reference [{}] is duplicated; not considering it for migration.", template);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VMTemplateVO templateVO = templateDao.findById(templateId);
|
||||||
|
if (!shouldMigrateTemplate(template, templateVO)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
files.add(templateFactory.getTemplate(template.getTemplateId(), srcDataStore));
|
files.add(templateFactory.getTemplate(template.getTemplateId(), srcDataStore));
|
||||||
|
idsForMigration.add(templateId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (TemplateInfo template: files) {
|
for (TemplateInfo template: files) {
|
||||||
List<VMTemplateVO> children = templateDao.listByParentTemplatetId(template.getId());
|
List<VMTemplateVO> children = templateDao.listByParentTemplatetId(template.getId());
|
||||||
List<TemplateInfo> temps = new ArrayList<>();
|
List<TemplateInfo> temps = new ArrayList<>();
|
||||||
|
|
@ -221,6 +233,7 @@ public class DataMigrationUtility {
|
||||||
}
|
}
|
||||||
childTemplates.put(template, new Pair<>(temps, getTotalChainSize(temps)));
|
childTemplates.put(template, new Pair<>(temps, getTotalChainSize(temps)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (List<DataObject>) (List<?>) files;
|
return (List<DataObject>) (List<?>) files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,16 +276,37 @@ public class DataMigrationUtility {
|
||||||
*/
|
*/
|
||||||
protected List<DataObject> getAllReadySnapshotsAndChains(DataStore srcDataStore, Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChains, List<SnapshotDataStoreVO> snapshots) {
|
protected List<DataObject> getAllReadySnapshotsAndChains(DataStore srcDataStore, Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChains, List<SnapshotDataStoreVO> snapshots) {
|
||||||
List<SnapshotInfo> files = new LinkedList<>();
|
List<SnapshotInfo> files = new LinkedList<>();
|
||||||
|
Set<Long> idsForMigration = new HashSet<>();
|
||||||
|
|
||||||
for (SnapshotDataStoreVO snapshot : snapshots) {
|
for (SnapshotDataStoreVO snapshot : snapshots) {
|
||||||
SnapshotVO snapshotVO = snapshotDao.findById(snapshot.getSnapshotId());
|
long snapshotId = snapshot.getSnapshotId();
|
||||||
if (snapshot.getState() == ObjectInDataStoreStateMachine.State.Ready &&
|
if (idsForMigration.contains(snapshotId)) {
|
||||||
snapshotVO != null && snapshotVO.getHypervisorType() != Hypervisor.HypervisorType.Simulator
|
logger.warn("Snapshot store reference [{}] is duplicated; not considering it for migration.", snapshot);
|
||||||
&& snapshot.getParentSnapshotId() == 0 ) {
|
continue;
|
||||||
|
}
|
||||||
|
if (snapshot.getState() != ObjectInDataStoreStateMachine.State.Ready) {
|
||||||
|
logger.warn("Not migrating snapshot [{}] because its state is not ready.", snapshot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SnapshotVO snapshotVO = snapshotDao.findById(snapshotId);
|
||||||
|
if (snapshotVO == null) {
|
||||||
|
logger.debug("Not migrating snapshot [{}] because we could not find its database entry.", snapshot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (snapshotVO.getHypervisorType() == Hypervisor.HypervisorType.Simulator) {
|
||||||
|
logger.debug("Not migrating snapshot [{}] because its hypervisor type is simulator.", snapshot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (snapshot.getParentSnapshotId() != 0) {
|
||||||
|
continue; // The child snapshot will be migrated in the for loop below.
|
||||||
|
}
|
||||||
SnapshotInfo snap = snapshotFactory.getSnapshot(snapshotVO.getSnapshotId(), snapshot.getDataStoreId(), snapshot.getRole());
|
SnapshotInfo snap = snapshotFactory.getSnapshot(snapshotVO.getSnapshotId(), snapshot.getDataStoreId(), snapshot.getRole());
|
||||||
if (snap != null) {
|
if (snap == null) {
|
||||||
|
logger.debug("Not migrating snapshot [{}] because we could not get its information.", snapshot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
files.add(snap);
|
files.add(snap);
|
||||||
}
|
idsForMigration.add(snapshotId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SnapshotInfo parent : files) {
|
for (SnapshotInfo parent : files) {
|
||||||
|
|
@ -285,7 +319,7 @@ public class DataMigrationUtility {
|
||||||
chain.addAll(children);
|
chain.addAll(children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snapshotChains.put(parent, new Pair<List<SnapshotInfo>, Long>(chain, getTotalChainSize(chain)));
|
snapshotChains.put(parent, new Pair<>(chain, getTotalChainSize(chain)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (List<DataObject>) (List<?>) files;
|
return (List<DataObject>) (List<?>) files;
|
||||||
|
|
@ -306,14 +340,31 @@ public class DataMigrationUtility {
|
||||||
|
|
||||||
protected List<DataObject> getAllReadyVolumes(DataStore srcDataStore, List<VolumeDataStoreVO> volumes) {
|
protected List<DataObject> getAllReadyVolumes(DataStore srcDataStore, List<VolumeDataStoreVO> volumes) {
|
||||||
List<DataObject> files = new LinkedList<>();
|
List<DataObject> files = new LinkedList<>();
|
||||||
|
Set<Long> idsForMigration = new HashSet<>();
|
||||||
|
|
||||||
for (VolumeDataStoreVO volume : volumes) {
|
for (VolumeDataStoreVO volume : volumes) {
|
||||||
if (volume.getState() == ObjectInDataStoreStateMachine.State.Ready) {
|
long volumeId = volume.getVolumeId();
|
||||||
|
if (idsForMigration.contains(volumeId)) {
|
||||||
|
logger.warn("Volume store reference [{}] is duplicated; not considering it for migration.", volume);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (volume.getState() != ObjectInDataStoreStateMachine.State.Ready) {
|
||||||
|
logger.debug("Not migrating volume [{}] because its state is not ready.", volume);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
VolumeInfo volumeInfo = volumeFactory.getVolume(volume.getVolumeId(), srcDataStore);
|
VolumeInfo volumeInfo = volumeFactory.getVolume(volume.getVolumeId(), srcDataStore);
|
||||||
if (volumeInfo != null && volumeInfo.getHypervisorType() != Hypervisor.HypervisorType.Simulator) {
|
if (volumeInfo == null) {
|
||||||
|
logger.debug("Not migrating volume [{}] because we could not get its information.", volume);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (volumeInfo.getHypervisorType() == Hypervisor.HypervisorType.Simulator) {
|
||||||
|
logger.debug("Not migrating volume [{}] because its hypervisor type is simulator.", volume);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
files.add(volumeInfo);
|
files.add(volumeInfo);
|
||||||
|
idsForMigration.add(volumeId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,10 +376,9 @@ public class DataMigrationUtility {
|
||||||
/** Returns the count of active SSVMs - SSVM with agents in connected state, so as to dynamically increase the thread pool
|
/** Returns the count of active SSVMs - SSVM with agents in connected state, so as to dynamically increase the thread pool
|
||||||
* size when SSVMs scale
|
* size when SSVMs scale
|
||||||
*/
|
*/
|
||||||
protected int activeSSVMCount(DataStore dataStore) {
|
protected int activeSSVMCount(Long zoneId) {
|
||||||
long datacenterId = dataStore.getScope().getScopeId();
|
|
||||||
List<SecondaryStorageVmVO> ssvms =
|
List<SecondaryStorageVmVO> ssvms =
|
||||||
secStorageVmDao.getSecStorageVmListInStates(null, datacenterId, VirtualMachine.State.Running, VirtualMachine.State.Migrating);
|
secStorageVmDao.getSecStorageVmListInStates(null, zoneId, VirtualMachine.State.Running, VirtualMachine.State.Migrating);
|
||||||
int activeSSVMs = 0;
|
int activeSSVMs = 0;
|
||||||
for (SecondaryStorageVmVO vm : ssvms) {
|
for (SecondaryStorageVmVO vm : ssvms) {
|
||||||
String name = "s-"+vm.getId()+"-VM";
|
String name = "s-"+vm.getId()+"-VM";
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageServic
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
|
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.SnapshotInfo;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
|
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;
|
||||||
import org.apache.cloudstack.framework.async.AsyncCallFuture;
|
import org.apache.cloudstack.framework.async.AsyncCallFuture;
|
||||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||||
import org.apache.cloudstack.framework.config.Configurable;
|
import org.apache.cloudstack.framework.config.Configurable;
|
||||||
|
|
@ -71,9 +73,12 @@ import com.cloud.storage.dao.SnapshotDao;
|
||||||
import com.cloud.utils.Pair;
|
import com.cloud.utils.Pair;
|
||||||
import com.cloud.utils.component.ManagerBase;
|
import com.cloud.utils.component.ManagerBase;
|
||||||
import com.cloud.utils.exception.CloudRuntimeException;
|
import com.cloud.utils.exception.CloudRuntimeException;
|
||||||
|
import org.apache.logging.log4j.ThreadContext;
|
||||||
|
|
||||||
public class StorageOrchestrator extends ManagerBase implements StorageOrchestrationService, Configurable {
|
public class StorageOrchestrator extends ManagerBase implements StorageOrchestrationService, Configurable {
|
||||||
|
|
||||||
|
private static final String LOGCONTEXTID = "logcontextid";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SnapshotDataStoreDao snapshotDataStoreDao;
|
SnapshotDataStoreDao snapshotDataStoreDao;
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -91,6 +96,8 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
@Inject
|
@Inject
|
||||||
private SecondaryStorageService secStgSrv;
|
private SecondaryStorageService secStgSrv;
|
||||||
@Inject
|
@Inject
|
||||||
|
TemplateService templateService;
|
||||||
|
@Inject
|
||||||
TemplateDataStoreDao templateDataStoreDao;
|
TemplateDataStoreDao templateDataStoreDao;
|
||||||
@Inject
|
@Inject
|
||||||
VolumeDataStoreDao volumeDataStoreDao;
|
VolumeDataStoreDao volumeDataStoreDao;
|
||||||
|
|
@ -106,6 +113,9 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
|
|
||||||
Integer numConcurrentCopyTasksPerSSVM = 2;
|
Integer numConcurrentCopyTasksPerSSVM = 2;
|
||||||
|
|
||||||
|
private final Map<Long, ThreadPoolExecutor> zoneExecutorMap = new HashMap<>();
|
||||||
|
private final Map<Long, Integer> zonePendingWorkCountMap = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getConfigComponentName() {
|
public String getConfigComponentName() {
|
||||||
return StorageOrchestrationService.class.getName();
|
return StorageOrchestrationService.class.getName();
|
||||||
|
|
@ -167,8 +177,6 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
double meanstddev = getStandardDeviation(storageCapacities);
|
double meanstddev = getStandardDeviation(storageCapacities);
|
||||||
double threshold = ImageStoreImbalanceThreshold.value();
|
double threshold = ImageStoreImbalanceThreshold.value();
|
||||||
MigrationResponse response = null;
|
MigrationResponse response = null;
|
||||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(numConcurrentCopyTasksPerSSVM , numConcurrentCopyTasksPerSSVM, 30,
|
|
||||||
TimeUnit.MINUTES, new MigrateBlockingQueue<>(numConcurrentCopyTasksPerSSVM));
|
|
||||||
Date start = new Date();
|
Date start = new Date();
|
||||||
if (meanstddev < threshold && migrationPolicy == MigrationPolicy.BALANCE) {
|
if (meanstddev < threshold && migrationPolicy == MigrationPolicy.BALANCE) {
|
||||||
logger.debug("mean std deviation of the image stores is below threshold, no migration required");
|
logger.debug("mean std deviation of the image stores is below threshold, no migration required");
|
||||||
|
|
@ -177,7 +185,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
|
|
||||||
int skipped = 0;
|
int skipped = 0;
|
||||||
List<Future<AsyncCallFuture<DataObjectResult>>> futures = new ArrayList<>();
|
List<Future<DataObjectResult>> futures = new ArrayList<>();
|
||||||
while (true) {
|
while (true) {
|
||||||
DataObject chosenFileForMigration = null;
|
DataObject chosenFileForMigration = null;
|
||||||
if (files.size() > 0) {
|
if (files.size() > 0) {
|
||||||
|
|
@ -206,7 +214,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMigrate(chosenFileForMigration, srcDatastore.getId(), destDatastoreId, storageCapacities, snapshotChains, childTemplates, migrationPolicy)) {
|
if (shouldMigrate(chosenFileForMigration, srcDatastore.getId(), destDatastoreId, storageCapacities, snapshotChains, childTemplates, migrationPolicy)) {
|
||||||
storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destDatastoreId, executor, futures);
|
storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destDatastoreId, futures);
|
||||||
} else {
|
} else {
|
||||||
if (migrationPolicy == MigrationPolicy.BALANCE) {
|
if (migrationPolicy == MigrationPolicy.BALANCE) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -217,7 +225,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Date end = new Date();
|
Date end = new Date();
|
||||||
handleSnapshotMigration(srcDataStoreId, start, end, migrationPolicy, futures, storageCapacities, executor);
|
handleSnapshotMigration(srcDataStoreId, start, end, migrationPolicy, futures, storageCapacities);
|
||||||
return handleResponse(futures, migrationPolicy, message, success);
|
return handleResponse(futures, migrationPolicy, message, success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,9 +258,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
storageCapacities = getStorageCapacities(storageCapacities, srcImgStoreId);
|
storageCapacities = getStorageCapacities(storageCapacities, srcImgStoreId);
|
||||||
storageCapacities = getStorageCapacities(storageCapacities, destImgStoreId);
|
storageCapacities = getStorageCapacities(storageCapacities, destImgStoreId);
|
||||||
|
|
||||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(numConcurrentCopyTasksPerSSVM, numConcurrentCopyTasksPerSSVM, 30,
|
List<Future<DataObjectResult>> futures = new ArrayList<>();
|
||||||
TimeUnit.MINUTES, new MigrateBlockingQueue<>(numConcurrentCopyTasksPerSSVM));
|
|
||||||
List<Future<AsyncCallFuture<DataObjectResult>>> futures = new ArrayList<>();
|
|
||||||
Date start = new Date();
|
Date start = new Date();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -272,7 +278,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storageCapacityBelowThreshold(storageCapacities, destImgStoreId)) {
|
if (storageCapacityBelowThreshold(storageCapacities, destImgStoreId)) {
|
||||||
storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destImgStoreId, executor, futures);
|
storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destImgStoreId, futures);
|
||||||
} else {
|
} else {
|
||||||
message = "Migration failed. Destination store doesn't have enough capacity for migration";
|
message = "Migration failed. Destination store doesn't have enough capacity for migration";
|
||||||
success = false;
|
success = false;
|
||||||
|
|
@ -289,7 +295,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snap.getSnapshotId(), snap.getDataStoreId(), DataStoreRole.Image);
|
SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snap.getSnapshotId(), snap.getDataStoreId(), DataStoreRole.Image);
|
||||||
SnapshotInfo parentSnapshot = snapshotInfo.getParent();
|
SnapshotInfo parentSnapshot = snapshotInfo.getParent();
|
||||||
if (snapshotInfo.getDataStore().getId() == srcImgStoreId && parentSnapshot != null && migratedSnapshotIdList.contains(parentSnapshot.getSnapshotId())) {
|
if (snapshotInfo.getDataStore().getId() == srcImgStoreId && parentSnapshot != null && migratedSnapshotIdList.contains(parentSnapshot.getSnapshotId())) {
|
||||||
futures.add(executor.submit(new MigrateDataTask(snapshotInfo, srcDatastore, dataStoreManager.getDataStore(destImgStoreId, DataStoreRole.Image))));
|
futures.add(submit(srcDatastore.getScope().getScopeId(), new MigrateDataTask(snapshotInfo, srcDatastore, dataStoreManager.getDataStore(destImgStoreId, DataStoreRole.Image))));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +303,11 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
return handleResponse(futures, null, message, success);
|
return handleResponse(futures, null, message, success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore) {
|
||||||
|
return submit(destStore.getScope().getScopeId(), new CopyTemplateTask(source, destStore));
|
||||||
|
}
|
||||||
|
|
||||||
protected Pair<String, Boolean> migrateCompleted(Long destDatastoreId, DataStore srcDatastore, List<DataObject> files, MigrationPolicy migrationPolicy, int skipped) {
|
protected Pair<String, Boolean> migrateCompleted(Long destDatastoreId, DataStore srcDatastore, List<DataObject> files, MigrationPolicy migrationPolicy, int skipped) {
|
||||||
String message = "";
|
String message = "";
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
|
|
@ -332,19 +343,10 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
Map<DataObject, Pair<List<TemplateInfo>, Long>> templateChains,
|
Map<DataObject, Pair<List<TemplateInfo>, Long>> templateChains,
|
||||||
DataStore srcDatastore,
|
DataStore srcDatastore,
|
||||||
Long destDatastoreId,
|
Long destDatastoreId,
|
||||||
ThreadPoolExecutor executor,
|
List<Future<DataObjectResult>> futures) {
|
||||||
List<Future<AsyncCallFuture<DataObjectResult>>> futures) {
|
|
||||||
|
|
||||||
Long fileSize = migrationHelper.getFileSize(chosenFileForMigration, snapshotChains, templateChains);
|
Long fileSize = migrationHelper.getFileSize(chosenFileForMigration, snapshotChains, templateChains);
|
||||||
|
|
||||||
storageCapacities = assumeMigrate(storageCapacities, srcDatastore.getId(), destDatastoreId, fileSize);
|
storageCapacities = assumeMigrate(storageCapacities, srcDatastore.getId(), destDatastoreId, fileSize);
|
||||||
long activeSsvms = migrationHelper.activeSSVMCount(srcDatastore);
|
|
||||||
long totalJobs = activeSsvms * numConcurrentCopyTasksPerSSVM;
|
|
||||||
// Increase thread pool size with increase in number of SSVMs
|
|
||||||
if ( totalJobs > executor.getCorePoolSize()) {
|
|
||||||
executor.setMaximumPoolSize((int) (totalJobs));
|
|
||||||
executor.setCorePoolSize((int) (totalJobs));
|
|
||||||
}
|
|
||||||
|
|
||||||
MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image));
|
MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image));
|
||||||
if (chosenFileForMigration instanceof SnapshotInfo ) {
|
if (chosenFileForMigration instanceof SnapshotInfo ) {
|
||||||
|
|
@ -353,19 +355,64 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
if (chosenFileForMigration instanceof TemplateInfo) {
|
if (chosenFileForMigration instanceof TemplateInfo) {
|
||||||
task.setTemplateChain(templateChains);
|
task.setTemplateChain(templateChains);
|
||||||
}
|
}
|
||||||
futures.add((executor.submit(task)));
|
futures.add(submit(srcDatastore.getScope().getScopeId(), task));
|
||||||
logger.debug("Migration of {}: {} is initiated.", chosenFileForMigration.getType().name(), chosenFileForMigration.getUuid());
|
logger.debug("Migration of {}: {} is initiated.", chosenFileForMigration.getType().name(), chosenFileForMigration.getUuid());
|
||||||
return storageCapacities;
|
return storageCapacities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected <T> Future<T> submit(Long zoneId, Callable<T> task) {
|
||||||
|
ThreadPoolExecutor executor;
|
||||||
|
synchronized (this) {
|
||||||
|
if (!zoneExecutorMap.containsKey(zoneId)) {
|
||||||
|
zoneExecutorMap.put(zoneId, new ThreadPoolExecutor(numConcurrentCopyTasksPerSSVM, numConcurrentCopyTasksPerSSVM,
|
||||||
|
30, TimeUnit.MINUTES, new MigrateBlockingQueue<>(numConcurrentCopyTasksPerSSVM)));
|
||||||
|
zonePendingWorkCountMap.put(zoneId, 0);
|
||||||
|
}
|
||||||
|
zonePendingWorkCountMap.merge(zoneId, 1, Integer::sum);
|
||||||
|
scaleExecutorIfNecessary(zoneId);
|
||||||
|
executor = zoneExecutorMap.get(zoneId);
|
||||||
|
}
|
||||||
|
return executor.submit(task);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private MigrationResponse handleResponse(List<Future<AsyncCallFuture<DataObjectResult>>> futures, MigrationPolicy migrationPolicy, String message, boolean success) {
|
protected void scaleExecutorIfNecessary(Long zoneId) {
|
||||||
|
long activeSsvms = migrationHelper.activeSSVMCount(zoneId);
|
||||||
|
long totalJobs = activeSsvms * numConcurrentCopyTasksPerSSVM;
|
||||||
|
ThreadPoolExecutor executor = zoneExecutorMap.get(zoneId);
|
||||||
|
if (totalJobs > executor.getCorePoolSize()) {
|
||||||
|
logger.debug("Scaling up executor of zone [{}] from [{}] to [{}] threads.", zoneId, executor.getCorePoolSize(),
|
||||||
|
totalJobs);
|
||||||
|
executor.setMaximumPoolSize((int) (totalJobs));
|
||||||
|
executor.setCorePoolSize((int) (totalJobs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized void tryCleaningUpExecutor(Long zoneId) {
|
||||||
|
if (!zoneExecutorMap.containsKey(zoneId)) {
|
||||||
|
logger.debug("No executor exists for zone [{}].", zoneId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
zonePendingWorkCountMap.merge(zoneId, -1, Integer::sum);
|
||||||
|
Integer pendingWorkCount = zonePendingWorkCountMap.get(zoneId);
|
||||||
|
if (pendingWorkCount > 0) {
|
||||||
|
logger.debug("Not cleaning executor of zone [{}] yet, as there is [{}] pending work.", zoneId, pendingWorkCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Cleaning executor of zone [{}].", zoneId);
|
||||||
|
ThreadPoolExecutor executor = zoneExecutorMap.get(zoneId);
|
||||||
|
zoneExecutorMap.remove(zoneId);
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MigrationResponse handleResponse(List<Future<DataObjectResult>> futures, MigrationPolicy migrationPolicy, String message, boolean success) {
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
for (Future<AsyncCallFuture<DataObjectResult>> future : futures) {
|
for (Future<DataObjectResult> future : futures) {
|
||||||
try {
|
try {
|
||||||
AsyncCallFuture<DataObjectResult> res = future.get();
|
DataObjectResult res = future.get();
|
||||||
if (res.get().isSuccess()) {
|
if (res.isSuccess()) {
|
||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
} catch ( InterruptedException | ExecutionException e) {
|
} catch ( InterruptedException | ExecutionException e) {
|
||||||
|
|
@ -379,7 +426,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSnapshotMigration(Long srcDataStoreId, Date start, Date end, MigrationPolicy policy,
|
private void handleSnapshotMigration(Long srcDataStoreId, Date start, Date end, MigrationPolicy policy,
|
||||||
List<Future<AsyncCallFuture<DataObjectResult>>> futures, Map<Long, Pair<Long, Long>> storageCapacities, ThreadPoolExecutor executor) {
|
List<Future<DataObjectResult>> futures, Map<Long, Pair<Long, Long>> storageCapacities) {
|
||||||
DataStore srcDatastore = dataStoreManager.getDataStore(srcDataStoreId, DataStoreRole.Image);
|
DataStore srcDatastore = dataStoreManager.getDataStore(srcDataStoreId, DataStoreRole.Image);
|
||||||
List<SnapshotDataStoreVO> snaps = snapshotDataStoreDao.findSnapshots(srcDataStoreId, start, end);
|
List<SnapshotDataStoreVO> snaps = snapshotDataStoreDao.findSnapshots(srcDataStoreId, start, end);
|
||||||
if (!snaps.isEmpty()) {
|
if (!snaps.isEmpty()) {
|
||||||
|
|
@ -395,12 +442,12 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
storeId = dstores.get(1);
|
storeId = dstores.get(1);
|
||||||
}
|
}
|
||||||
DataStore datastore = dataStoreManager.getDataStore(storeId, DataStoreRole.Image);
|
DataStore datastore = dataStoreManager.getDataStore(storeId, DataStoreRole.Image);
|
||||||
futures.add(executor.submit(new MigrateDataTask(snapshotInfo, srcDatastore, datastore)));
|
futures.add(submit(srcDatastore.getScope().getScopeId(), new MigrateDataTask(snapshotInfo, srcDatastore, datastore)));
|
||||||
}
|
}
|
||||||
if (parentSnapshot != null) {
|
if (parentSnapshot != null) {
|
||||||
DataStore parentDS = dataStoreManager.getDataStore(parentSnapshot.getDataStore().getId(), DataStoreRole.Image);
|
DataStore parentDS = dataStoreManager.getDataStore(parentSnapshot.getDataStore().getId(), DataStoreRole.Image);
|
||||||
if (parentDS.getId() != snapshotInfo.getDataStore().getId()) {
|
if (parentDS.getId() != snapshotInfo.getDataStore().getId()) {
|
||||||
futures.add(executor.submit(new MigrateDataTask(snapshotInfo, srcDatastore, parentDS)));
|
futures.add(submit(srcDatastore.getScope().getScopeId(), new MigrateDataTask(snapshotInfo, srcDatastore, parentDS)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -527,16 +574,19 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
return standardDeviation.evaluate(metricValues, mean);
|
return standardDeviation.evaluate(metricValues, mean);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MigrateDataTask implements Callable<AsyncCallFuture<DataObjectResult>> {
|
private class MigrateDataTask implements Callable<DataObjectResult> {
|
||||||
private DataObject file;
|
private DataObject file;
|
||||||
private DataStore srcDataStore;
|
private DataStore srcDataStore;
|
||||||
private DataStore destDataStore;
|
private DataStore destDataStore;
|
||||||
private Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChain;
|
private Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChain;
|
||||||
private Map<DataObject, Pair<List<TemplateInfo>, Long>> templateChain;
|
private Map<DataObject, Pair<List<TemplateInfo>, Long>> templateChain;
|
||||||
|
private String logid;
|
||||||
|
|
||||||
public MigrateDataTask(DataObject file, DataStore srcDataStore, DataStore destDataStore) {
|
public MigrateDataTask(DataObject file, DataStore srcDataStore, DataStore destDataStore) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.srcDataStore = srcDataStore;
|
this.srcDataStore = srcDataStore;
|
||||||
this.destDataStore = destDataStore;
|
this.destDataStore = destDataStore;
|
||||||
|
this.logid = ThreadContext.get(LOGCONTEXTID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSnapshotChains(Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChain) {
|
public void setSnapshotChains(Map<DataObject, Pair<List<SnapshotInfo>, Long>> snapshotChain) {
|
||||||
|
|
@ -557,8 +607,50 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AsyncCallFuture<DataObjectResult> call() throws Exception {
|
public DataObjectResult call() {
|
||||||
return secStgSrv.migrateData(file, srcDataStore, destDataStore, snapshotChain, templateChain);
|
ThreadContext.put(LOGCONTEXTID, logid);
|
||||||
|
DataObjectResult result;
|
||||||
|
AsyncCallFuture<DataObjectResult> future = secStgSrv.migrateData(file, srcDataStore, destDataStore, snapshotChain, templateChain);
|
||||||
|
try {
|
||||||
|
result = future.get();
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
logger.warn("Exception while migrating data to another secondary storage: {}", e.toString());
|
||||||
|
result = new DataObjectResult(file);
|
||||||
|
result.setResult(e.toString());
|
||||||
|
}
|
||||||
|
tryCleaningUpExecutor(srcDataStore.getScope().getScopeId());
|
||||||
|
ThreadContext.clearAll();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CopyTemplateTask implements Callable<TemplateApiResult> {
|
||||||
|
private TemplateInfo sourceTmpl;
|
||||||
|
private DataStore destStore;
|
||||||
|
private String logid;
|
||||||
|
|
||||||
|
public CopyTemplateTask(TemplateInfo sourceTmpl, DataStore destStore) {
|
||||||
|
this.sourceTmpl = sourceTmpl;
|
||||||
|
this.destStore = destStore;
|
||||||
|
this.logid = ThreadContext.get(LOGCONTEXTID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TemplateApiResult call() {
|
||||||
|
ThreadContext.put(LOGCONTEXTID, logid);
|
||||||
|
TemplateApiResult result;
|
||||||
|
AsyncCallFuture<TemplateApiResult> future = templateService.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());
|
||||||
|
}
|
||||||
|
tryCleaningUpExecutor(destStore.getScope().getScopeId());
|
||||||
|
ThreadContext.clearAll();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
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.CopyCommandResult;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
|
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionService;
|
import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionService;
|
||||||
|
|
@ -42,7 +43,6 @@ import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
|
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event;
|
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State;
|
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
|
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
|
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager;
|
import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager;
|
||||||
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
|
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
|
||||||
|
|
@ -58,6 +58,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||||
import org.apache.cloudstack.framework.messagebus.MessageBus;
|
import org.apache.cloudstack.framework.messagebus.MessageBus;
|
||||||
import org.apache.cloudstack.framework.messagebus.PublishScope;
|
import org.apache.cloudstack.framework.messagebus.PublishScope;
|
||||||
import org.apache.cloudstack.storage.command.CommandResult;
|
import org.apache.cloudstack.storage.command.CommandResult;
|
||||||
|
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
|
||||||
import org.apache.cloudstack.storage.command.DeleteCommand;
|
import org.apache.cloudstack.storage.command.DeleteCommand;
|
||||||
import org.apache.cloudstack.storage.datastore.DataObjectManager;
|
import org.apache.cloudstack.storage.datastore.DataObjectManager;
|
||||||
import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager;
|
import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager;
|
||||||
|
|
@ -77,7 +78,6 @@ import com.cloud.agent.api.storage.ListTemplateCommand;
|
||||||
import com.cloud.agent.api.to.DatadiskTO;
|
import com.cloud.agent.api.to.DatadiskTO;
|
||||||
import com.cloud.alert.AlertManager;
|
import com.cloud.alert.AlertManager;
|
||||||
import com.cloud.configuration.Config;
|
import com.cloud.configuration.Config;
|
||||||
import com.cloud.configuration.Resource;
|
|
||||||
import com.cloud.configuration.Resource.ResourceType;
|
import com.cloud.configuration.Resource.ResourceType;
|
||||||
import com.cloud.dc.DataCenterVO;
|
import com.cloud.dc.DataCenterVO;
|
||||||
import com.cloud.dc.dao.ClusterDao;
|
import com.cloud.dc.dao.ClusterDao;
|
||||||
|
|
@ -157,6 +157,8 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
ImageStoreDetailsUtil imageStoreDetailsUtil;
|
ImageStoreDetailsUtil imageStoreDetailsUtil;
|
||||||
@Inject
|
@Inject
|
||||||
TemplateDataFactory imageFactory;
|
TemplateDataFactory imageFactory;
|
||||||
|
@Inject
|
||||||
|
StorageOrchestrationService storageOrchestrator;
|
||||||
|
|
||||||
class TemplateOpContext<T> extends AsyncRpcContext<T> {
|
class TemplateOpContext<T> extends AsyncRpcContext<T> {
|
||||||
final TemplateObject template;
|
final TemplateObject template;
|
||||||
|
|
@ -320,7 +322,6 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
if (syncLock.lock(3)) {
|
if (syncLock.lock(3)) {
|
||||||
try {
|
try {
|
||||||
Long zoneId = store.getScope().getScopeId();
|
Long zoneId = store.getScope().getScopeId();
|
||||||
|
|
||||||
Map<String, TemplateProp> templateInfos = listTemplate(store);
|
Map<String, TemplateProp> templateInfos = listTemplate(store);
|
||||||
if (templateInfos == null) {
|
if (templateInfos == null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -529,10 +530,6 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
availHypers.add(HypervisorType.None); // bug 9809: resume ISO
|
availHypers.add(HypervisorType.None); // bug 9809: resume ISO
|
||||||
// download.
|
// download.
|
||||||
for (VMTemplateVO tmplt : toBeDownloaded) {
|
for (VMTemplateVO tmplt : toBeDownloaded) {
|
||||||
if (tmplt.getUrl() == null) { // If url is null, skip downloading
|
|
||||||
logger.info("Skip downloading template {} since no url is specified.", tmplt);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// if this is private template, skip sync to a new image store
|
// if this is private template, skip sync to a new image store
|
||||||
if (isSkipTemplateStoreDownload(tmplt, zoneId)) {
|
if (isSkipTemplateStoreDownload(tmplt, zoneId)) {
|
||||||
logger.info("Skip sync downloading private template {} to a new image store", tmplt);
|
logger.info("Skip sync downloading private template {} to a new image store", tmplt);
|
||||||
|
|
@ -551,14 +548,10 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (availHypers.contains(tmplt.getHypervisorType())) {
|
if (availHypers.contains(tmplt.getHypervisorType())) {
|
||||||
logger.info("Downloading template {} to image store {}", tmplt, store);
|
boolean copied = isCopyFromOtherStoragesEnabled(zoneId) && tryCopyingTemplateToImageStore(tmplt, store);
|
||||||
associateTemplateToZone(tmplt.getId(), zoneId);
|
if (!copied) {
|
||||||
TemplateInfo tmpl = _templateFactory.getTemplate(tmplt.getId(), store);
|
tryDownloadingTemplateToImageStore(tmplt, store);
|
||||||
TemplateOpContext<TemplateApiResult> context = new TemplateOpContext<>(null,(TemplateObject)tmpl, null);
|
}
|
||||||
AsyncCallbackDispatcher<TemplateServiceImpl, TemplateApiResult> caller = AsyncCallbackDispatcher.create(this);
|
|
||||||
caller.setCallback(caller.getTarget().createTemplateAsyncCallBack(null, null));
|
|
||||||
caller.setContext(context);
|
|
||||||
createTemplateAsync(tmpl, store, caller);
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("Skip downloading template {} since current data center does not have hypervisor {}", tmplt, tmplt.getHypervisorType());
|
logger.info("Skip downloading template {} since current data center does not have hypervisor {}", tmplt, tmplt.getHypervisorType());
|
||||||
}
|
}
|
||||||
|
|
@ -605,6 +598,127 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
destStore.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Downloading template [{}] to image store [{}].", tmplt.getUniqueName(), destStore.getName());
|
||||||
|
associateTemplateToZone(tmplt.getId(), destStore.getScope().getScopeId());
|
||||||
|
TemplateInfo tmpl = _templateFactory.getTemplate(tmplt.getId(), destStore);
|
||||||
|
TemplateOpContext<TemplateApiResult> context = new TemplateOpContext<>(null,(TemplateObject)tmpl, null);
|
||||||
|
AsyncCallbackDispatcher<TemplateServiceImpl, TemplateApiResult> caller = AsyncCallbackDispatcher.create(this);
|
||||||
|
caller.setCallback(caller.getTarget().createTemplateAsyncCallBack(null, null));
|
||||||
|
caller.setContext(context);
|
||||||
|
createTemplateAsync(tmpl, destStore, caller);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
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());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
storageOrchestrator.orchestrateTemplateCopyToImageStore(sourceTmpl, destStore);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.debug("Can't copy template [{}] from another image store.", tmplt.getUniqueName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject source, DataStore destStore) {
|
||||||
|
TemplateObject sourceTmpl = (TemplateObject) source;
|
||||||
|
logger.debug("Copying template [{}] from image store [{}] to [{}].", sourceTmpl.getUniqueName(), sourceTmpl.getDataStore().getName(),
|
||||||
|
destStore.getName());
|
||||||
|
TemplateObject destTmpl = (TemplateObject) destStore.create(sourceTmpl);
|
||||||
|
destTmpl.processEvent(Event.CreateOnlyRequested);
|
||||||
|
|
||||||
|
AsyncCallFuture<TemplateApiResult> future = new AsyncCallFuture<>();
|
||||||
|
TemplateOpContext<TemplateApiResult> context = new TemplateOpContext<>(null, destTmpl, future);
|
||||||
|
AsyncCallbackDispatcher<TemplateServiceImpl, CopyCommandResult> caller = AsyncCallbackDispatcher.create(this);
|
||||||
|
caller.setCallback(caller.getTarget().copyTemplateToImageStoreCallback(null, null)).setContext(context);
|
||||||
|
|
||||||
|
if (source.getDataStore().getId() == destStore.getId()) {
|
||||||
|
logger.debug("Destination image store [{}] is the same as the origin; returning success to normalize the metadata.");
|
||||||
|
CopyCmdAnswer answer = new CopyCmdAnswer(source.getTO());
|
||||||
|
CopyCommandResult result = new CopyCommandResult("", answer);
|
||||||
|
caller.complete(result);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
_motionSrv.copyAsync(sourceTmpl, destTmpl, caller);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Void copyTemplateToImageStoreCallback(AsyncCallbackDispatcher<TemplateServiceImpl, CopyCommandResult> callback, TemplateOpContext<TemplateApiResult> context) {
|
||||||
|
TemplateInfo tmplt = context.getTemplate();
|
||||||
|
CopyCommandResult result = callback.getResult();
|
||||||
|
AsyncCallFuture<TemplateApiResult> future = context.getFuture();
|
||||||
|
TemplateApiResult res = new TemplateApiResult(tmplt);
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
logger.info("Copied template [{}] to image store [{}].", tmplt.getUniqueName(), tmplt.getDataStore().getName());
|
||||||
|
tmplt.processEvent(Event.OperationSuccessed, result.getAnswer());
|
||||||
|
publishTemplateCreation(tmplt);
|
||||||
|
} else {
|
||||||
|
logger.warn("Failed to copy template [{}] to image store [{}].", tmplt.getUniqueName(), tmplt.getDataStore().getName());
|
||||||
|
res.setResult(result.getResult());
|
||||||
|
tmplt.processEvent(Event.OperationFailed);
|
||||||
|
}
|
||||||
|
future.complete(res);
|
||||||
|
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());
|
||||||
|
|
||||||
|
if (tmpltVo.isPublicTemplate()) {
|
||||||
|
_messageBus.publish(null, TemplateManager.MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT, PublishScope.LOCAL, tmpltVo.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Long size = tmplt.getSize();
|
||||||
|
if (size == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataStore store = tmplt.getDataStore();
|
||||||
|
TemplateDataStoreVO tmpltStore = _vmTemplateStoreDao.findByStoreTemplate(store.getId(), tmpltVo.getId());
|
||||||
|
|
||||||
|
long physicalSize = 0;
|
||||||
|
if (tmpltStore != null) {
|
||||||
|
physicalSize = tmpltStore.getPhysicalSize();
|
||||||
|
} else {
|
||||||
|
logger.warn("No entry found in template_store_ref for template [{}] and image store [{}] at the end of registering template!",
|
||||||
|
tmpltVo.getUniqueName(), store.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Long zoneId = store.getScope().getScopeId();
|
||||||
|
if (zoneId != null) {
|
||||||
|
String usageEvent = tmplt.getFormat() == ImageFormat.ISO ? EventTypes.EVENT_ISO_CREATE : EventTypes.EVENT_TEMPLATE_CREATE;
|
||||||
|
UsageEventUtils.publishUsageEvent(usageEvent, tmpltVo.getAccountId(), zoneId, tmpltVo.getId(), tmpltVo.getName(),
|
||||||
|
null, null, physicalSize, size, VirtualMachineTemplate.class.getName(), tmpltVo.getUuid());
|
||||||
|
} else {
|
||||||
|
logger.warn("Zone-wide image store [{}] has a null scope ID.", store);
|
||||||
|
}
|
||||||
|
|
||||||
|
_resourceLimitMgr.incrementResourceCount(tmpltVo.getAccountId(), ResourceType.secondary_storage, size);
|
||||||
|
}
|
||||||
|
|
||||||
// persist entry in template_zone_ref table. zoneId can be empty for
|
// persist entry in template_zone_ref table. zoneId can be empty for
|
||||||
// region-wide image store, in that case,
|
// region-wide image store, in that case,
|
||||||
// we will associate the template to all the zones.
|
// we will associate the template to all the zones.
|
||||||
|
|
@ -650,45 +764,14 @@ public class TemplateServiceImpl implements TemplateService {
|
||||||
|
|
||||||
protected Void createTemplateAsyncCallBack(AsyncCallbackDispatcher<TemplateServiceImpl, TemplateApiResult> callback,
|
protected Void createTemplateAsyncCallBack(AsyncCallbackDispatcher<TemplateServiceImpl, TemplateApiResult> callback,
|
||||||
TemplateOpContext<TemplateApiResult> context) {
|
TemplateOpContext<TemplateApiResult> context) {
|
||||||
TemplateInfo template = context.template;
|
|
||||||
TemplateApiResult result = callback.getResult();
|
TemplateApiResult result = callback.getResult();
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
VMTemplateVO tmplt = _templateDao.findById(template.getId());
|
publishTemplateCreation(context.template);
|
||||||
// need to grant permission for public templates
|
|
||||||
if (tmplt.isPublicTemplate()) {
|
|
||||||
_messageBus.publish(null, TemplateManager.MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT, PublishScope.LOCAL, tmplt.getId());
|
|
||||||
}
|
}
|
||||||
long accountId = tmplt.getAccountId();
|
|
||||||
if (template.getSize() != null) {
|
|
||||||
// publish usage event
|
|
||||||
String etype = EventTypes.EVENT_TEMPLATE_CREATE;
|
|
||||||
if (tmplt.getFormat() == ImageFormat.ISO) {
|
|
||||||
etype = EventTypes.EVENT_ISO_CREATE;
|
|
||||||
}
|
|
||||||
// get physical size from template_store_ref table
|
|
||||||
long physicalSize = 0;
|
|
||||||
DataStore ds = template.getDataStore();
|
|
||||||
TemplateDataStoreVO tmpltStore = _vmTemplateStoreDao.findByStoreTemplate(ds.getId(), template.getId());
|
|
||||||
if (tmpltStore != null) {
|
|
||||||
physicalSize = tmpltStore.getPhysicalSize();
|
|
||||||
} else {
|
|
||||||
logger.warn("No entry found in template_store_ref for template: {} and image store: {} at the end of registering template!", template, ds);
|
|
||||||
}
|
|
||||||
Scope dsScope = ds.getScope();
|
|
||||||
if (dsScope.getScopeId() != null) {
|
|
||||||
UsageEventUtils.publishUsageEvent(etype, template.getAccountId(), dsScope.getScopeId(), template.getId(), template.getName(), null, null,
|
|
||||||
physicalSize, template.getSize(), VirtualMachineTemplate.class.getName(), template.getUuid());
|
|
||||||
} else {
|
|
||||||
logger.warn("Zone scope image store {} has a null scope id", ds);
|
|
||||||
}
|
|
||||||
_resourceLimitMgr.incrementResourceCount(accountId, Resource.ResourceType.secondary_storage, template.getSize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, TemplateProp> listTemplate(DataStore ssStore) {
|
protected Map<String, TemplateProp> listTemplate(DataStore ssStore) {
|
||||||
String nfsVersion = imageStoreDetailsUtil.getNfsVersion(ssStore.getId());
|
String nfsVersion = imageStoreDetailsUtil.getNfsVersion(ssStore.getId());
|
||||||
ListTemplateCommand cmd = new ListTemplateCommand(ssStore.getTO(), nfsVersion);
|
ListTemplateCommand cmd = new ListTemplateCommand(ssStore.getTO(), nfsVersion);
|
||||||
EndPoint ep = _epSelector.select(ssStore);
|
EndPoint ep = _epSelector.select(ssStore);
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,17 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cloudstack.storage.image;
|
package org.apache.cloudstack.storage.image;
|
||||||
|
|
||||||
|
import com.cloud.storage.template.TemplateProp;
|
||||||
|
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.TemplateDataStoreDao;
|
||||||
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
|
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
|
||||||
|
import org.apache.cloudstack.storage.image.store.TemplateObject;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
|
@ -33,6 +41,10 @@ import com.cloud.storage.DataStoreRole;
|
||||||
import com.cloud.storage.Storage;
|
import com.cloud.storage.Storage;
|
||||||
import com.cloud.storage.VMTemplateVO;
|
import com.cloud.storage.VMTemplateVO;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
public class TemplateServiceImplTest {
|
public class TemplateServiceImplTest {
|
||||||
|
|
||||||
|
|
@ -43,6 +55,49 @@ public class TemplateServiceImplTest {
|
||||||
@Mock
|
@Mock
|
||||||
TemplateDataStoreDao templateDataStoreDao;
|
TemplateDataStoreDao templateDataStoreDao;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
TemplateDataFactoryImpl templateDataFactoryMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
DataStoreManager dataStoreManagerMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
VMTemplateVO tmpltMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
TemplateProp tmpltPropMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
TemplateObject templateInfoMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
DataStore sourceStoreMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
DataStore destStoreMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
Scope zoneScopeMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
StorageOrchestrationService storageOrchestrator;
|
||||||
|
|
||||||
|
Map<String, TemplateProp> templatesInSourceStore = new HashMap<>();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
Long zoneId = 1L;
|
||||||
|
Mockito.doReturn(2L).when(tmpltMock).getId();
|
||||||
|
Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
|
||||||
|
Mockito.doReturn(zoneId).when(zoneScopeMock).getScopeId();
|
||||||
|
Mockito.doReturn(zoneScopeMock).when(destStoreMock).getScope();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsSkipTemplateStoreDownloadPublicTemplate() {
|
public void testIsSkipTemplateStoreDownloadPublicTemplate() {
|
||||||
VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class);
|
VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class);
|
||||||
|
|
@ -81,4 +136,51 @@ public class TemplateServiceImplTest {
|
||||||
Mockito.when(templateDataStoreDao.findByTemplateZone(id, id, DataStoreRole.Image)).thenReturn(Mockito.mock(TemplateDataStoreVO.class));
|
Mockito.when(templateDataStoreDao.findByTemplateZone(id, id, DataStoreRole.Image)).thenReturn(Mockito.mock(TemplateDataStoreVO.class));
|
||||||
Assert.assertTrue(templateService.isSkipTemplateStoreDownload(templateVO, id));
|
Assert.assertTrue(templateService.isSkipTemplateStoreDownload(templateVO, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tryDownloadingTemplateToImageStoreTestDownloadsTemplateWhenUrlIsNotNull() {
|
||||||
|
Mockito.doReturn("url").when(tmpltMock).getUrl();
|
||||||
|
Mockito.doNothing().when(templateService).associateTemplateToZone(Mockito.anyLong(), Mockito.any(Long.class));
|
||||||
|
|
||||||
|
templateService.tryDownloadingTemplateToImageStore(tmpltMock, destStoreMock);
|
||||||
|
|
||||||
|
Mockito.verify(templateService).createTemplateAsync(Mockito.any(), Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tryDownloadingTemplateToImageStoreTestDoesNothingWhenUrlIsNull() {
|
||||||
|
templateService.tryDownloadingTemplateToImageStore(tmpltMock, destStoreMock);
|
||||||
|
|
||||||
|
Mockito.verify(templateService, Mockito.never()).createTemplateAsync(Mockito.any(), Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenTemplateDoesNotExistOnAnotherImageStore() {
|
||||||
|
boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
|
||||||
|
|
||||||
|
Assert.assertFalse(result);
|
||||||
|
Mockito.verify(storageOrchestrator, Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenInstallPathIsNull() {
|
||||||
|
templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
|
||||||
|
Mockito.doReturn(null).when(templateInfoMock).getInstallPath();
|
||||||
|
|
||||||
|
boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
|
||||||
|
|
||||||
|
Assert.assertFalse(result);
|
||||||
|
Mockito.verify(storageOrchestrator, Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherStorageAndTaskWasScheduled() {
|
||||||
|
templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
|
||||||
|
Mockito.doReturn(new AsyncCallFuture<>()).when(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(), Mockito.any());
|
||||||
|
|
||||||
|
boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
|
||||||
|
|
||||||
|
Assert.assertTrue(result);
|
||||||
|
Mockito.verify(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4194,7 +4194,8 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
|
||||||
VmwareAllowParallelExecution,
|
VmwareAllowParallelExecution,
|
||||||
DataStoreDownloadFollowRedirects,
|
DataStoreDownloadFollowRedirects,
|
||||||
AllowVolumeReSizeBeyondAllocation,
|
AllowVolumeReSizeBeyondAllocation,
|
||||||
StoragePoolHostConnectWorkers
|
StoragePoolHostConnectWorkers,
|
||||||
|
COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue