diff --git a/api/src/main/java/com/cloud/network/VirtualNetworkApplianceService.java b/api/src/main/java/com/cloud/network/VirtualNetworkApplianceService.java index 8504efda509..92a664f7f95 100644 --- a/api/src/main/java/com/cloud/network/VirtualNetworkApplianceService.java +++ b/api/src/main/java/com/cloud/network/VirtualNetworkApplianceService.java @@ -44,7 +44,7 @@ public interface VirtualNetworkApplianceService { * the command specifying router's id * @return router if successful */ - VirtualRouter rebootRouter(long routerId, boolean reprogramNetwork) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; + VirtualRouter rebootRouter(long routerId, boolean reprogramNetwork, boolean forced) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; VirtualRouter upgradeRouter(UpgradeRouterCmd cmd); diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index b1fc60d8cf2..75c88e3fa08 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -334,6 +334,7 @@ public class ApiConstants { public static final String SNAPSHOT_POLICY_ID = "snapshotpolicyid"; public static final String SNAPSHOT_TYPE = "snapshottype"; public static final String SNAPSHOT_QUIESCEVM = "quiescevm"; + public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; public static final String START_DATE = "startdate"; public static final String START_ID = "startid"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java index 802e3df3dcf..1bae2547c35 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java @@ -49,6 +49,9 @@ public class RebootRouterCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DomainRouterResponse.class, required = true, description = "the ID of the router") private Long id; + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false, description = "Force reboot the router (Router is force Stopped and then Started)") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -96,10 +99,14 @@ public class RebootRouterCmd extends BaseAsyncCmd { return getId(); } + public boolean isForced() { + return (forced != null) ? forced : false; + } + @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { CallContext.current().setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class,getId())); - VirtualRouter result = _routerService.rebootRouter(getId(), true); + VirtualRouter result = _routerService.rebootRouter(getId(), true, isForced()); if (result != null) { DomainRouterResponse response = _responseGenerator.createDomainRouterResponse(result); response.setResponseName("router"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java index ebc50ae7e1d..39f43c3afae 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java @@ -52,6 +52,9 @@ public class RebootSystemVmCmd extends BaseAsyncCmd { description = "The ID of the system virtual machine") private Long id; + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false, description = "Force reboot the system VM (System VM is Stopped and then Started)") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -104,6 +107,10 @@ public class RebootSystemVmCmd extends BaseAsyncCmd { return getId(); } + public boolean isForced() { + return (forced != null) ? forced : false; + } + @Override public void execute() { CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java index 6011bdbcffe..42a686b8c04 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java @@ -53,6 +53,9 @@ public class RebootVMCmd extends BaseAsyncCmd implements UserCmd { required=true, description="The ID of the virtual machine") private Long id; + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false, description = "Force reboot the VM (VM is Stopped and then Started)") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -61,6 +64,10 @@ public class RebootVMCmd extends BaseAsyncCmd implements UserCmd { return id; } + public boolean isForced() { + return (forced != null) ? forced : false; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java index 1cdd69673cf..e0de6314f5d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java @@ -248,8 +248,12 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co @Param(description = "need quiesce vm or not when taking snapshot", since = "4.3") private boolean needQuiescevm; + @SerializedName(ApiConstants.SUPPORTS_STORAGE_SNAPSHOT) + @Param(description = "true if storage snapshot is supported for the volume, false otherwise") + private boolean supportsStorageSnapshot; + @SerializedName(ApiConstants.PHYSICAL_SIZE) - @Param(description = "the bytes alloaated") + @Param(description = "the bytes allocated") private Long physicalsize; @SerializedName(ApiConstants.VIRTUAL_SIZE) @@ -538,6 +542,14 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co return this.needQuiescevm; } + public void setSupportsStorageSnapshot(boolean supportsStorageSnapshot) { + this.supportsStorageSnapshot = supportsStorageSnapshot; + } + + public boolean getSupportsStorageSnapshot() { + return this.supportsStorageSnapshot; + } + public String getIsoId() { return isoId; } diff --git a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java index 17b9b700d6c..97f74cc0927 100644 --- a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java +++ b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java @@ -96,10 +96,13 @@ public class StorageSubsystemCommandHandlerBase implements StorageSubsystemComma //copy volume from image cache to primary return processor.copyVolumeFromImageCacheToPrimary(cmd); } else if (srcData.getObjectType() == DataObjectType.VOLUME && srcData.getDataStore().getRole() == DataStoreRole.Primary) { - if (destData.getObjectType() == DataObjectType.VOLUME && srcData instanceof VolumeObjectTO && ((VolumeObjectTO)srcData).isDirectDownload()) { - return processor.copyVolumeFromPrimaryToPrimary(cmd); - } else if (destData.getObjectType() == DataObjectType.VOLUME) { - return processor.copyVolumeFromPrimaryToSecondary(cmd); + if (destData.getObjectType() == DataObjectType.VOLUME) { + if ((srcData instanceof VolumeObjectTO && ((VolumeObjectTO)srcData).isDirectDownload()) || + destData.getDataStore().getRole() == DataStoreRole.Primary) { + return processor.copyVolumeFromPrimaryToPrimary(cmd); + } else { + return processor.copyVolumeFromPrimaryToSecondary(cmd); + } } else if (destData.getObjectType() == DataObjectType.TEMPLATE) { return processor.createTemplateFromVolume(cmd); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java index f4a73810901..723e2349285 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java @@ -22,6 +22,7 @@ import com.cloud.agent.api.Answer; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.DiskOffering.DiskCacheMode; import com.cloud.storage.MigrationOptions; +import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.vm.VirtualMachine; @@ -35,6 +36,8 @@ public interface VolumeInfo extends DataObject, Volume { HypervisorType getHypervisorType(); + Storage.StoragePoolType getStoragePoolType(); + Long getLastPoolId(); String getAttachedVmName(); 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 13eccd7f35a..b1a1af5f03a 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 @@ -186,6 +186,8 @@ public interface StorageManager extends StorageService { StoragePoolVO findLocalStorageOnHost(long hostId); + Host findUpAndEnabledHostWithAccessToStoragePools(List poolIds); + List findStoragePoolsConnectedToHost(long hostId); boolean canHostAccessStoragePool(Host host, StoragePool pool); @@ -230,7 +232,9 @@ public interface StorageManager extends StorageService { */ boolean storagePoolHasEnoughSpace(List volume, StoragePool pool, Long clusterId); - boolean storagePoolHasEnoughSpaceForResize(StoragePool pool, long currentSize, long newSiz); + boolean storagePoolHasEnoughSpaceForResize(StoragePool pool, long currentSize, long newSize); + + boolean storagePoolCompatibleWithVolumePool(StoragePool pool, Volume volume); boolean registerHostListener(String providerUuid, HypervisorHostListener listener); diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index f096f8f0394..fe95ef08b09 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3191,7 +3191,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } return; } - s_logger.info("Unable to reboot VM " + vm + " on " + dest.getHost() + " due to " + (rebootAnswer == null ? " no reboot answer" : rebootAnswer.getDetails())); + + String errorMsg = "Unable to reboot VM " + vm + " on " + dest.getHost() + " due to " + (rebootAnswer == null ? "no reboot response" : rebootAnswer.getDetails()); + s_logger.info(errorMsg); + throw new CloudRuntimeException(errorMsg); } catch (final OperationTimedoutException e) { s_logger.warn("Unable to send the reboot command to host " + dest.getHost() + " for the vm " + vm + " due to operation timeout", e); throw new CloudRuntimeException("Failed to reboot the vm on host " + dest.getHost()); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 57f3f09111f..cebf15c1b22 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -1050,6 +1050,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati VolumeApiResult result = future.get(); if (result.isFailed()) { s_logger.error("Migrate volume failed:" + result.getResult()); + if (result.getResult() != null && result.getResult().contains("[UNSUPPORTED]")) { + throw new CloudRuntimeException("Migrate volume failed: " + result.getResult()); + } throw new StorageUnavailableException("Migrate volume failed: " + result.getResult(), destPool.getId()); } else { // update the volumeId for snapshots on secondary diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDao.java index 8dd10a7c29a..b099a6d6bdb 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDao.java @@ -32,6 +32,8 @@ public interface StoragePoolHostDao extends GenericDao List listByHostStatus(long poolId, Status hostStatus); + List findHostsConnectedToPools(List poolIds); + List> getDatacenterStoragePoolHostInfo(long dcId, boolean sharedOnly); public void deletePrimaryRecordsForHost(long hostId); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDaoImpl.java index 2b7b0f7cbe4..c27aeb0f652 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/StoragePoolHostDaoImpl.java @@ -21,7 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; - +import java.util.stream.Collectors; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -44,6 +44,8 @@ public class StoragePoolHostDaoImpl extends GenericDaoBase findHostsConnectedToPools(List poolIds) { + List hosts = new ArrayList(); + if (poolIds == null || poolIds.isEmpty()) { + return hosts; + } + + String poolIdsInStr = poolIds.stream().map(poolId -> String.valueOf(poolId)).collect(Collectors.joining(",", "(", ")")); + String sql = HOSTS_FOR_POOLS_SEARCH.replace("(?)", poolIdsInStr); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try(PreparedStatement pstmt = txn.prepareStatement(sql);) { + try(ResultSet rs = pstmt.executeQuery();) { + while (rs.next()) { + long hostId = rs.getLong(1); // host_id column + hosts.add(hostId); + } + } catch (SQLException e) { + s_logger.warn("findHostsConnectedToPools:Exception: ", e); + } + } catch (Exception e) { + s_logger.warn("findHostsConnectedToPools:Exception: ", e); + } + + return hosts; + } + @Override public List> getDatacenterStoragePoolHostInfo(long dcId, boolean sharedOnly) { ArrayList> l = new ArrayList>(); diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index 2bf30dbe347..a4cff378885 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -574,6 +574,14 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } } + private void verifyFormatWithPoolType(ImageFormat imageFormat, StoragePoolType poolType) { + if (imageFormat != ImageFormat.VHD && imageFormat != ImageFormat.OVA && imageFormat != ImageFormat.QCOW2 && + !(imageFormat == ImageFormat.RAW && StoragePoolType.PowerFlex == poolType)) { + throw new CloudRuntimeException("Only the following image types are currently supported: " + + ImageFormat.VHD.toString() + ", " + ImageFormat.OVA.toString() + ", " + ImageFormat.QCOW2.toString() + ", and " + ImageFormat.RAW.toString() + "(for PowerFlex)"); + } + } + private void verifyFormat(ImageFormat imageFormat) { if (imageFormat != ImageFormat.VHD && imageFormat != ImageFormat.OVA && imageFormat != ImageFormat.QCOW2) { throw new CloudRuntimeException("Only the following image types are currently supported: " + @@ -585,8 +593,9 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { long volumeId = snapshotInfo.getVolumeId(); VolumeVO volumeVO = _volumeDao.findByIdIncludingRemoved(volumeId); + StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeVO.getPoolId()); - verifyFormat(volumeVO.getFormat()); + verifyFormatWithPoolType(volumeVO.getFormat(), storagePoolVO.getPoolType()); } private boolean usingBackendSnapshotFor(SnapshotInfo snapshotInfo) { @@ -917,6 +926,11 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { boolean keepGrantedAccess = false; DataStore srcDataStore = snapshotInfo.getDataStore(); + StoragePoolVO storagePoolVO = _storagePoolDao.findById(srcDataStore.getId()); + + if (HypervisorType.KVM.equals(snapshotInfo.getHypervisorType()) && storagePoolVO.getPoolType() == StoragePoolType.PowerFlex) { + usingBackendSnapshot = false; + } if (usingBackendSnapshot) { createVolumeFromSnapshot(snapshotInfo); @@ -1310,7 +1324,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { Preconditions.checkArgument(volumeInfo != null, "Passing 'null' to volumeInfo of " + "handleCreateVolumeFromTemplateBothOnStorageSystem is not supported."); - verifyFormat(templateInfo.getFormat()); + DataStore dataStore = volumeInfo.getDataStore(); + if (dataStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(dataStore.getId()); + verifyFormatWithPoolType(templateInfo.getFormat(), storagePoolVO.getPoolType()); + } else { + verifyFormat(templateInfo.getFormat()); + } HostVO hostVO = null; @@ -2305,7 +2325,8 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { CopyCmdAnswer copyCmdAnswer = null; try { - if (!ImageFormat.QCOW2.equals(volumeInfo.getFormat())) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (!ImageFormat.QCOW2.equals(volumeInfo.getFormat()) && !(ImageFormat.RAW.equals(volumeInfo.getFormat()) && StoragePoolType.PowerFlex == storagePoolVO.getPoolType())) { throw new CloudRuntimeException("When using managed storage, you can only create a template from a volume on KVM currently."); } @@ -2321,7 +2342,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { try { handleQualityOfServiceForVolumeMigration(volumeInfo, PrimaryDataStoreDriver.QualityOfServiceState.MIGRATION); - if (srcVolumeDetached) { + if (srcVolumeDetached || StoragePoolType.PowerFlex == storagePoolVO.getPoolType()) { _volumeService.grantAccess(volumeInfo, hostVO, srcDataStore); } @@ -2353,7 +2374,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { throw new CloudRuntimeException(msg + ex.getMessage(), ex); } finally { - if (srcVolumeDetached) { + if (srcVolumeDetached || StoragePoolType.PowerFlex == storagePoolVO.getPoolType()) { try { _volumeService.revokeAccess(volumeInfo, hostVO, srcDataStore); } @@ -2448,7 +2469,12 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { long snapshotId = snapshotInfo.getId(); - snapshotDetails.put(DiskTO.IQN, getSnapshotProperty(snapshotId, DiskTO.IQN)); + if (storagePoolVO.getPoolType() == StoragePoolType.PowerFlex) { + snapshotDetails.put(DiskTO.IQN, snapshotInfo.getPath()); + } else { + snapshotDetails.put(DiskTO.IQN, getSnapshotProperty(snapshotId, DiskTO.IQN)); + } + snapshotDetails.put(DiskTO.VOLUME_SIZE, String.valueOf(snapshotInfo.getSize())); snapshotDetails.put(DiskTO.SCSI_NAA_DEVICE_ID, getSnapshotProperty(snapshotId, DiskTO.SCSI_NAA_DEVICE_ID)); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java index b837e60687b..d2e2ec3bf58 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java @@ -141,7 +141,7 @@ public class ScaleIOVMSnapshotStrategy extends ManagerBase implements VMSnapshot for (VolumeObjectTO volume : volumeTOs) { String volumeSnapshotName = String.format("%s-%s-%s-%s-%s", ScaleIOUtil.VMSNAPSHOT_PREFIX, vmSnapshotVO.getId(), volume.getId(), storagePool.getUuid().split("-")[0].substring(4), ManagementServerImpl.customCsIdentifier.value()); - srcVolumeDestSnapshotMap.put(volume.getPath(), volumeSnapshotName); + srcVolumeDestSnapshotMap.put(ScaleIOUtil.getVolumePath(volume.getPath()), volumeSnapshotName); virtual_size += volume.getSize(); VolumeVO volumeVO = volumeDao.findById(volume.getId()); @@ -173,7 +173,9 @@ public class ScaleIOVMSnapshotStrategy extends ManagerBase implements VMSnapshot vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), "SnapshotGroupId", snapshotGroupId, false)); for (int index = 0; index < volumeIds.size(); index++) { - vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), "Vol_" + volumeTOs.get(index).getId() + "_Snapshot", volumeIds.get(index), false)); + String volumeSnapshotName = srcVolumeDestSnapshotMap.get(ScaleIOUtil.getVolumePath(volumeTOs.get(index).getPath())); + String pathWithScaleIOVolumeName = ScaleIOUtil.updatedPathWithVolumeName(volumeIds.get(index), volumeSnapshotName); + vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), "Vol_" + volumeTOs.get(index).getId() + "_Snapshot", pathWithScaleIOVolumeName, false)); } vmSnapshotDetailsDao.saveDetails(vmSnapshotDetails); @@ -265,8 +267,8 @@ public class ScaleIOVMSnapshotStrategy extends ManagerBase implements VMSnapshot Map srcSnapshotDestVolumeMap = new HashMap<>(); for (VolumeObjectTO volume : volumeTOs) { VMSnapshotDetailsVO vmSnapshotDetail = vmSnapshotDetailsDao.findDetail(vmSnapshotVO.getId(), "Vol_" + volume.getId() + "_Snapshot"); - String srcSnapshotVolumeId = vmSnapshotDetail.getValue(); - String destVolumeId = volume.getPath(); + String srcSnapshotVolumeId = ScaleIOUtil.getVolumePath(vmSnapshotDetail.getValue()); + String destVolumeId = ScaleIOUtil.getVolumePath(volume.getPath()); srcSnapshotDestVolumeMap.put(srcSnapshotVolumeId, destVolumeId); } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java index 727d10af130..539f7811993 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java @@ -207,12 +207,16 @@ public abstract class AbstractStoragePoolAllocator extends AdapterBase implement return false; } + Volume volume = volumeDao.findById(dskCh.getVolumeId()); + if(!storageMgr.storagePoolCompatibleWithVolumePool(pool, volume)) { + return false; + } + if (pool.isManaged() && !storageUtil.managedStoragePoolCanScale(pool, plan.getClusterId(), plan.getHostId())) { return false; } // check capacity - Volume volume = volumeDao.findById(dskCh.getVolumeId()); List requestVolumes = new ArrayList<>(); requestVolumes.add(volume); return storageMgr.storagePoolHasEnoughIops(requestVolumes, pool) && storageMgr.storagePoolHasEnoughSpace(requestVolumes, pool, plan.getClusterId()); diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/ZoneWideStoragePoolAllocator.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/ZoneWideStoragePoolAllocator.java index 301704a75a6..225f781489c 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/ZoneWideStoragePoolAllocator.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/ZoneWideStoragePoolAllocator.java @@ -48,15 +48,10 @@ public class ZoneWideStoragePoolAllocator extends AbstractStoragePoolAllocator { @Inject private CapacityDao capacityDao; - @Override protected List select(DiskProfile dskCh, VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, int returnUpTo) { LOGGER.debug("ZoneWideStoragePoolAllocator to find storage pool"); - if (dskCh.useLocalStorage()) { - return null; - } - if (LOGGER.isTraceEnabled()) { // Log the pools details that are ignored because they are in disabled state List disabledPools = storagePoolDao.findDisabledPoolsByScope(plan.getDataCenterId(), null, null, ScopeType.ZONE); @@ -92,7 +87,6 @@ public class ZoneWideStoragePoolAllocator extends AbstractStoragePoolAllocator { avoid.addPool(pool.getId()); } - for (StoragePoolVO storage : storagePools) { if (suitablePools.size() == returnUpTo) { break; @@ -114,7 +108,6 @@ public class ZoneWideStoragePoolAllocator extends AbstractStoragePoolAllocator { return !ScopeType.ZONE.equals(storagePoolVO.getScope()) || !storagePoolVO.isManaged(); } - @Override protected List reorderPoolsByCapacity(DeploymentPlan plan, List pools) { diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/PrimaryDataStoreImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/PrimaryDataStoreImpl.java index 81966784be0..d322ce62f14 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/PrimaryDataStoreImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/PrimaryDataStoreImpl.java @@ -203,8 +203,7 @@ public class PrimaryDataStoreImpl implements PrimaryDataStore { @Override public String getName() { - // TODO Auto-generated method stub - return null; + return pdsv.getName(); } @Override diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java index 690a1124402..83ff1e11e38 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java @@ -20,9 +20,6 @@ import java.util.Date; import javax.inject.Inject; -import com.cloud.storage.MigrationOptions; -import org.apache.log4j.Logger; - import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; @@ -33,6 +30,7 @@ import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.log4j.Logger; import com.cloud.agent.api.Answer; import com.cloud.agent.api.storage.DownloadAnswer; @@ -42,6 +40,8 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.DiskOffering.DiskCacheMode; import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.MigrationOptions; +import com.cloud.storage.Storage; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.ProvisioningType; import com.cloud.storage.Volume; @@ -588,6 +588,11 @@ public class VolumeObject implements VolumeInfo { return volumeDao.getHypervisorType(volumeVO.getId()); } + @Override + public Storage.StoragePoolType getStoragePoolType() { + return volumeVO.getPoolType(); + } + @Override public Long getLastPoolId() { return volumeVO.getLastPoolId(); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 9d3591136fb..57682134697 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -64,6 +64,8 @@ import org.apache.cloudstack.storage.datastore.PrimaryDataStoreProviderManager; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; @@ -126,6 +128,7 @@ import com.cloud.utils.db.DB; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; +import com.google.common.base.Strings; @Component public class VolumeServiceImpl implements VolumeService { @@ -165,6 +168,8 @@ public class VolumeServiceImpl implements VolumeService { @Inject private PrimaryDataStoreDao storagePoolDao; @Inject + private StoragePoolDetailsDao _storagePoolDetailsDao; + @Inject private HostDetailsDao hostDetailsDao; @Inject private ManagementService mgr; @@ -176,6 +181,8 @@ public class VolumeServiceImpl implements VolumeService { private TemplateDataFactory tmplFactory; @Inject private VolumeOrchestrationService _volumeMgr; + @Inject + private StorageManager _storageMgr; private final static String SNAPSHOT_ID = "SNAPSHOT_ID"; @@ -1589,6 +1596,8 @@ public class VolumeServiceImpl implements VolumeService { // part here to make sure the credentials do not get stored in the db unencrypted. if (pool.getPoolType() == StoragePoolType.SMB && folder != null && folder.contains("?")) { folder = folder.substring(0, folder.indexOf("?")); + } else if (pool.getPoolType() == StoragePoolType.PowerFlex) { + folder = volume.getFolder(); } VolumeVO newVol = new VolumeVO(volume); @@ -1598,6 +1607,7 @@ public class VolumeServiceImpl implements VolumeService { newVol.setFolder(folder); newVol.setPodId(pool.getPodId()); newVol.setPoolId(pool.getId()); + newVol.setPoolType(pool.getPoolType()); newVol.setLastPoolId(lastPoolId); newVol.setPodId(pool.getPodId()); return volDao.persist(newVol); @@ -1614,7 +1624,6 @@ public class VolumeServiceImpl implements VolumeService { this.destVolume = destVolume; this.future = future; } - } protected AsyncCallFuture copyVolumeFromImageToPrimary(VolumeInfo srcVolume, DataStore destStore) { @@ -1724,8 +1733,8 @@ public class VolumeServiceImpl implements VolumeService { @Override public AsyncCallFuture copyVolume(VolumeInfo srcVolume, DataStore destStore) { + DataStore srcStore = srcVolume.getDataStore(); if (s_logger.isDebugEnabled()) { - DataStore srcStore = srcVolume.getDataStore(); String srcRole = (srcStore != null && srcStore.getRole() != null ? srcVolume.getDataStore().getRole().toString() : ""); String msg = String.format("copying %s(id=%d, role=%s) to %s (id=%d, role=%s)" @@ -1746,6 +1755,11 @@ public class VolumeServiceImpl implements VolumeService { return copyVolumeFromPrimaryToImage(srcVolume, destStore); } + if (srcStore.getRole() == DataStoreRole.Primary && destStore.getRole() == DataStoreRole.Primary && ((PrimaryDataStore) destStore).isManaged() && + requiresNewManagedVolumeInDestStore((PrimaryDataStore) srcStore, (PrimaryDataStore) destStore)) { + return copyManagedVolume(srcVolume, destStore); + } + // OfflineVmwareMigration: aren't we missing secondary to secondary in this logic? AsyncCallFuture future = new AsyncCallFuture(); @@ -1791,6 +1805,14 @@ public class VolumeServiceImpl implements VolumeService { destVolume.processEvent(Event.MigrationCopyFailed); srcVolume.processEvent(Event.OperationFailed); destroyVolume(destVolume.getId()); + if (destVolume.getStoragePoolType() == StoragePoolType.PowerFlex) { + s_logger.info("Dest volume " + destVolume.getId() + " can be removed"); + destVolume.processEvent(Event.ExpungeRequested); + destVolume.processEvent(Event.OperationSuccessed); + volDao.remove(destVolume.getId()); + future.complete(res); + return null; + } destVolume = volFactory.getVolume(destVolume.getId()); AsyncCallFuture destroyFuture = expungeVolumeAsync(destVolume); destroyFuture.get(); @@ -1801,6 +1823,14 @@ public class VolumeServiceImpl implements VolumeService { volDao.updateUuid(srcVolume.getId(), destVolume.getId()); try { destroyVolume(srcVolume.getId()); + if (srcVolume.getStoragePoolType() == StoragePoolType.PowerFlex) { + s_logger.info("Src volume " + srcVolume.getId() + " can be removed"); + srcVolume.processEvent(Event.ExpungeRequested); + srcVolume.processEvent(Event.OperationSuccessed); + volDao.remove(srcVolume.getId()); + future.complete(res); + return null; + } srcVolume = volFactory.getVolume(srcVolume.getId()); AsyncCallFuture destroyFuture = expungeVolumeAsync(srcVolume); // If volume destroy fails, this could be because of vdi is still in use state, so wait and retry. @@ -1823,6 +1853,213 @@ public class VolumeServiceImpl implements VolumeService { return null; } + private class CopyManagedVolumeContext extends AsyncRpcContext { + final VolumeInfo srcVolume; + final VolumeInfo destVolume; + final Host host; + final AsyncCallFuture future; + + public CopyManagedVolumeContext(AsyncCompletionCallback callback, AsyncCallFuture future, VolumeInfo srcVolume, VolumeInfo destVolume, Host host) { + super(callback); + this.srcVolume = srcVolume; + this.destVolume = destVolume; + this.host = host; + this.future = future; + } + } + + private AsyncCallFuture copyManagedVolume(VolumeInfo srcVolume, DataStore destStore) { + AsyncCallFuture future = new AsyncCallFuture(); + VolumeApiResult res = new VolumeApiResult(srcVolume); + try { + if (!snapshotMgr.canOperateOnVolume(srcVolume)) { + s_logger.debug("There are snapshots creating for this volume, can not move this volume"); + res.setResult("There are snapshots creating for this volume, can not move this volume"); + future.complete(res); + return future; + } + + if (snapshotMgr.backedUpSnapshotsExistsForVolume(srcVolume)) { + s_logger.debug("There are backed up snapshots for this volume, can not move."); + res.setResult("[UNSUPPORTED] There are backed up snapshots for this volume, can not move. Please try again after removing them."); + future.complete(res); + return future; + } + + List poolIds = new ArrayList(); + poolIds.add(srcVolume.getPoolId()); + poolIds.add(destStore.getId()); + + Host hostWithPoolsAccess = _storageMgr.findUpAndEnabledHostWithAccessToStoragePools(poolIds); + if (hostWithPoolsAccess == null) { + s_logger.debug("No host(s) available with pool access, can not move this volume"); + res.setResult("No host(s) available with pool access, can not move this volume"); + future.complete(res); + return future; + } + + VolumeVO destVol = duplicateVolumeOnAnotherStorage(srcVolume, (StoragePool)destStore); + VolumeInfo destVolume = volFactory.getVolume(destVol.getId(), destStore); + + // Create a volume on managed storage. + AsyncCallFuture createVolumeFuture = createVolumeAsync(destVolume, destStore); + VolumeApiResult createVolumeResult = createVolumeFuture.get(); + if (createVolumeResult.isFailed()) { + throw new CloudRuntimeException("Creation of a dest volume failed: " + createVolumeResult.getResult()); + } + + // Refresh the volume info from the DB. + destVolume = volFactory.getVolume(destVolume.getId(), destStore); + + PrimaryDataStore srcPrimaryDataStore = (PrimaryDataStore) srcVolume.getDataStore(); + if (srcPrimaryDataStore.isManaged()) { + Map srcPrimaryDataStoreDetails = new HashMap(); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED, Boolean.TRUE.toString()); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.STORAGE_HOST, srcPrimaryDataStore.getHostAddress()); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.STORAGE_PORT, String.valueOf(srcPrimaryDataStore.getPort())); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET, srcVolume.get_iScsiName()); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET_ROOT_VOLUME, srcVolume.getName()); + srcPrimaryDataStoreDetails.put(PrimaryDataStore.VOLUME_SIZE, String.valueOf(srcVolume.getSize())); + srcPrimaryDataStoreDetails.put(StorageManager.STORAGE_POOL_DISK_WAIT.toString(), String.valueOf(StorageManager.STORAGE_POOL_DISK_WAIT.valueIn(srcPrimaryDataStore.getId()))); + srcPrimaryDataStore.setDetails(srcPrimaryDataStoreDetails); + grantAccess(srcVolume, hostWithPoolsAccess, srcVolume.getDataStore()); + } + + PrimaryDataStore destPrimaryDataStore = (PrimaryDataStore) destStore; + Map destPrimaryDataStoreDetails = new HashMap(); + destPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED, Boolean.TRUE.toString()); + destPrimaryDataStoreDetails.put(PrimaryDataStore.STORAGE_HOST, destPrimaryDataStore.getHostAddress()); + destPrimaryDataStoreDetails.put(PrimaryDataStore.STORAGE_PORT, String.valueOf(destPrimaryDataStore.getPort())); + destPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET, destVolume.get_iScsiName()); + destPrimaryDataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET_ROOT_VOLUME, destVolume.getName()); + destPrimaryDataStoreDetails.put(PrimaryDataStore.VOLUME_SIZE, String.valueOf(destVolume.getSize())); + destPrimaryDataStoreDetails.put(StorageManager.STORAGE_POOL_DISK_WAIT.toString(), String.valueOf(StorageManager.STORAGE_POOL_DISK_WAIT.valueIn(destPrimaryDataStore.getId()))); + destPrimaryDataStore.setDetails(destPrimaryDataStoreDetails); + + grantAccess(destVolume, hostWithPoolsAccess, destStore); + + destVolume.processEvent(Event.CreateRequested); + srcVolume.processEvent(Event.MigrationRequested); + + CopyManagedVolumeContext context = new CopyManagedVolumeContext(null, future, srcVolume, destVolume, hostWithPoolsAccess); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().copyManagedVolumeCallBack(null, null)).setContext(context); + + motionSrv.copyAsync(srcVolume, destVolume, hostWithPoolsAccess, caller); + } catch (Exception e) { + s_logger.error("Copy to managed volume failed due to: " + e); + if(s_logger.isDebugEnabled()) { + s_logger.debug("Copy to managed volume failed.", e); + } + res.setResult(e.toString()); + future.complete(res); + } + + return future; + } + + protected Void copyManagedVolumeCallBack(AsyncCallbackDispatcher callback, CopyManagedVolumeContext context) { + VolumeInfo srcVolume = context.srcVolume; + VolumeInfo destVolume = context.destVolume; + Host host = context.host; + CopyCommandResult result = callback.getResult(); + AsyncCallFuture future = context.future; + VolumeApiResult res = new VolumeApiResult(destVolume); + + try { + if (srcVolume.getDataStore() != null && ((PrimaryDataStore) srcVolume.getDataStore()).isManaged()) { + revokeAccess(srcVolume, host, srcVolume.getDataStore()); + } + revokeAccess(destVolume, host, destVolume.getDataStore()); + + if (result.isFailed()) { + res.setResult(result.getResult()); + destVolume.processEvent(Event.MigrationCopyFailed); + srcVolume.processEvent(Event.OperationFailed); + try { + destroyVolume(destVolume.getId()); + destVolume = volFactory.getVolume(destVolume.getId()); + AsyncCallFuture destVolumeDestroyFuture = expungeVolumeAsync(destVolume); + destVolumeDestroyFuture.get(); + // If dest managed volume destroy fails, wait and retry. + if (destVolumeDestroyFuture.get().isFailed()) { + Thread.sleep(5 * 1000); + destVolumeDestroyFuture = expungeVolumeAsync(destVolume); + destVolumeDestroyFuture.get(); + } + future.complete(res); + } catch (Exception e) { + s_logger.debug("failed to clean up managed volume on storage", e); + } + } else { + srcVolume.processEvent(Event.OperationSuccessed); + destVolume.processEvent(Event.MigrationCopySucceeded, result.getAnswer()); + volDao.updateUuid(srcVolume.getId(), destVolume.getId()); + try { + destroyVolume(srcVolume.getId()); + srcVolume = volFactory.getVolume(srcVolume.getId()); + AsyncCallFuture srcVolumeDestroyFuture = expungeVolumeAsync(srcVolume); + // If src volume destroy fails, wait and retry. + if (srcVolumeDestroyFuture.get().isFailed()) { + Thread.sleep(5 * 1000); + srcVolumeDestroyFuture = expungeVolumeAsync(srcVolume); + srcVolumeDestroyFuture.get(); + } + future.complete(res); + } catch (Exception e) { + s_logger.debug("failed to clean up volume on storage", e); + } + } + } catch (Exception e) { + s_logger.debug("Failed to process copy managed volume callback", e); + res.setResult(e.toString()); + future.complete(res); + } + + return null; + } + + private boolean requiresNewManagedVolumeInDestStore(PrimaryDataStore srcDataStore, PrimaryDataStore destDataStore) { + if (srcDataStore == null || destDataStore == null) { + s_logger.warn("Unable to check for new volume, either src or dest pool is null"); + return false; + } + + if (srcDataStore.getPoolType() == StoragePoolType.PowerFlex && destDataStore.getPoolType() == StoragePoolType.PowerFlex) { + if (srcDataStore.getId() == destDataStore.getId()) { + return false; + } + + final String STORAGE_POOL_SYSTEM_ID = "powerflex.storagepool.system.id"; + String srcPoolSystemId = null; + StoragePoolDetailVO srcPoolSystemIdDetail = _storagePoolDetailsDao.findDetail(srcDataStore.getId(), STORAGE_POOL_SYSTEM_ID); + if (srcPoolSystemIdDetail != null) { + srcPoolSystemId = srcPoolSystemIdDetail.getValue(); + } + + String destPoolSystemId = null; + StoragePoolDetailVO destPoolSystemIdDetail = _storagePoolDetailsDao.findDetail(destDataStore.getId(), STORAGE_POOL_SYSTEM_ID); + if (destPoolSystemIdDetail != null) { + destPoolSystemId = destPoolSystemIdDetail.getValue(); + } + + if (Strings.isNullOrEmpty(srcPoolSystemId) || Strings.isNullOrEmpty(destPoolSystemId)) { + s_logger.warn("PowerFlex src pool: " + srcDataStore.getId() + " or dest pool: " + destDataStore.getId() + + " storage instance details are not available"); + return false; + } + + if (!srcPoolSystemId.equals(destPoolSystemId)) { + s_logger.debug("PowerFlex src pool: " + srcDataStore.getId() + " and dest pool: " + destDataStore.getId() + + " belongs to different storage instances, create new managed volume"); + return true; + } + } + + // New volume not required for all other cases (address any cases required in future) + return false; + } + private class MigrateVolumeContext extends AsyncRpcContext { final VolumeInfo srcVolume; final VolumeInfo destVolume; @@ -1858,7 +2095,7 @@ public class VolumeServiceImpl implements VolumeService { caller.setCallback(caller.getTarget().migrateVolumeCallBack(null, null)).setContext(context); motionSrv.copyAsync(srcVolume, destVolume, caller); } catch (Exception e) { - s_logger.debug("Failed to copy volume", e); + s_logger.debug("Failed to migrate volume", e); res.setResult(e.toString()); future.complete(res); } @@ -1877,6 +2114,10 @@ public class VolumeServiceImpl implements VolumeService { future.complete(res); } else { srcVolume.processEvent(Event.OperationSuccessed); + if (srcVolume.getStoragePoolType() == StoragePoolType.PowerFlex) { + future.complete(res); + return null; + } snapshotMgr.cleanupSnapshotsByVolume(srcVolume.getId()); future.complete(res); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index dc4a800ddb7..e386a22d7f0 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -131,7 +131,6 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.GuestDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.GuestResourceDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InputDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef.GuestNetType; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.RngDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.RngDef.RngBackendModel; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.SCSIDef; @@ -3216,35 +3215,15 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv String msg = null; try { dm = conn.domainLookupByName(vmName); - // Get XML Dump including the secure information such as VNC password - // By passing 1, or VIR_DOMAIN_XML_SECURE flag - // https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainXMLFlags - String vmDef = dm.getXMLDesc(1); - final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); - parser.parseDomainXML(vmDef); - for (final InterfaceDef nic : parser.getInterfaces()) { - if (nic.getNetType() == GuestNetType.BRIDGE && nic.getBrName().startsWith("cloudVirBr")) { - try { - final int vnetId = Integer.parseInt(nic.getBrName().replaceFirst("cloudVirBr", "")); - final String pifName = getPif(_guestBridgeName); - final String newBrName = "br" + pifName + "-" + vnetId; - vmDef = vmDef.replace("'" + nic.getBrName() + "'", "'" + newBrName + "'"); - s_logger.debug("VM bridge name is changed from " + nic.getBrName() + " to " + newBrName); - } catch (final NumberFormatException e) { - continue; - } - } - } - s_logger.debug(vmDef); - msg = stopVM(conn, vmName, false); - msg = startVM(conn, vmName, vmDef); + // Perform ACPI based reboot + // https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainReboot + // https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainRebootFlagValues + // Send ACPI event to Reboot + dm.reboot(0x1); return null; } catch (final LibvirtException e) { s_logger.warn("Failed to create vm", e); msg = e.getMessage(); - } catch (final InternalErrorException e) { - s_logger.warn("Failed to create vm", e); - msg = e.getMessage(); } finally { try { if (dm != null) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index c785354bf28..4f1d35a0301 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -1829,23 +1829,56 @@ public class KVMStorageProcessor implements StorageProcessor { final ImageFormat destFormat = destVol.getFormat(); final DataStoreTO srcStore = srcData.getDataStore(); final DataStoreTO destStore = destData.getDataStore(); - final PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)srcStore; - final PrimaryDataStoreTO primaryStoreDest = (PrimaryDataStoreTO)destStore; + final PrimaryDataStoreTO srcPrimaryStore = (PrimaryDataStoreTO)srcStore; + final PrimaryDataStoreTO destPrimaryStore = (PrimaryDataStoreTO)destStore; final String srcVolumePath = srcData.getPath(); final String destVolumePath = destData.getPath(); KVMStoragePool destPool = null; try { - final String volumeName = UUID.randomUUID().toString(); + s_logger.debug("Copying src volume (id: " + srcVol.getId() + ", format: " + srcFormat + ", path: " + srcVolumePath + ", primary storage: [id: " + srcPrimaryStore.getId() + ", type: " + srcPrimaryStore.getPoolType() + "]) to dest volume (id: " + + destVol.getId() + ", format: " + destFormat + ", path: " + destVolumePath + ", primary storage: [id: " + destPrimaryStore.getId() + ", type: " + destPrimaryStore.getPoolType() + "])."); + + if (srcPrimaryStore.isManaged()) { + if (!storagePoolMgr.connectPhysicalDisk(srcPrimaryStore.getPoolType(), srcPrimaryStore.getUuid(), srcVolumePath, srcPrimaryStore.getDetails())) { + s_logger.warn("Failed to connect src volume at path: " + srcVolumePath + ", in storage pool id: " + srcPrimaryStore.getUuid()); + } + } + + final KVMPhysicalDisk volume = storagePoolMgr.getPhysicalDisk(srcPrimaryStore.getPoolType(), srcPrimaryStore.getUuid(), srcVolumePath); + if (volume == null) { + s_logger.debug("Failed to get physical disk for volume: " + srcVolumePath); + throw new CloudRuntimeException("Failed to get physical disk for volume at path: " + srcVolumePath); + } - final String destVolumeName = volumeName + "." + destFormat.getFileExtension(); - final KVMPhysicalDisk volume = storagePoolMgr.getPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), srcVolumePath); volume.setFormat(PhysicalDiskFormat.valueOf(srcFormat.toString())); - destPool = storagePoolMgr.getStoragePool(primaryStoreDest.getPoolType(), primaryStoreDest.getUuid()); + String destVolumeName = null; + if (destPrimaryStore.isManaged()) { + if (!storagePoolMgr.connectPhysicalDisk(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid(), destVolumePath, destPrimaryStore.getDetails())) { + s_logger.warn("Failed to connect dest volume at path: " + destVolumePath + ", in storage pool id: " + destPrimaryStore.getUuid()); + } + String managedStoreTarget = destPrimaryStore.getDetails() != null ? destPrimaryStore.getDetails().get("managedStoreTarget") : null; + destVolumeName = managedStoreTarget != null ? managedStoreTarget : destVolumePath; + } else { + final String volumeName = UUID.randomUUID().toString(); + destVolumeName = volumeName + "." + destFormat.getFileExtension(); + } + + destPool = storagePoolMgr.getStoragePool(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid()); storagePoolMgr.copyPhysicalDisk(volume, destVolumeName, destPool, cmd.getWaitInMillSeconds()); + + if (srcPrimaryStore.isManaged()) { + storagePoolMgr.disconnectPhysicalDisk(srcPrimaryStore.getPoolType(), srcPrimaryStore.getUuid(), srcVolumePath); + } + + if (destPrimaryStore.isManaged()) { + storagePoolMgr.disconnectPhysicalDisk(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid(), destVolumePath); + } + final VolumeObjectTO newVol = new VolumeObjectTO(); - newVol.setPath(destVolumePath + File.separator + destVolumeName); + String path = destPrimaryStore.isManaged() ? destVolumeName : destVolumePath + File.separator + destVolumeName; + newVol.setPath(path); newVol.setFormat(destFormat); return new CopyCmdAnswer(newVol); } catch (final CloudRuntimeException e) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java index 419fa0cb2d4..62eb5440468 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java @@ -67,12 +67,14 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { } @Override - public KVMPhysicalDisk getPhysicalDisk(String volumeId, KVMStoragePool pool) { - if (Strings.isNullOrEmpty(volumeId) || pool == null) { - LOGGER.error("Unable to get physical disk, unspecified volumeid or pool"); + public KVMPhysicalDisk getPhysicalDisk(String volumePath, KVMStoragePool pool) { + if (Strings.isNullOrEmpty(volumePath) || pool == null) { + LOGGER.error("Unable to get physical disk, volume path or pool not specified"); return null; } + String volumeId = ScaleIOUtil.getVolumePath(volumePath); + try { String diskFilePath = null; String systemId = ScaleIOUtil.getSystemIdForVolume(volumeId); @@ -98,7 +100,7 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { } } - KVMPhysicalDisk disk = new KVMPhysicalDisk(diskFilePath, volumeId, pool); + KVMPhysicalDisk disk = new KVMPhysicalDisk(diskFilePath, volumePath, pool); disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); long diskSize = getPhysicalDiskSize(diskFilePath); @@ -107,8 +109,8 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { return disk; } catch (Exception e) { - LOGGER.error("Failed to get the physical disk: " + volumeId + " on the storage pool: " + pool.getUuid() + " due to " + e.getMessage()); - throw new CloudRuntimeException("Failed to get the physical disk: " + volumeId + " on the storage pool: " + pool.getUuid()); + LOGGER.error("Failed to get the physical disk: " + volumePath + " on the storage pool: " + pool.getUuid() + " due to " + e.getMessage()); + throw new CloudRuntimeException("Failed to get the physical disk: " + volumePath + " on the storage pool: " + pool.getUuid()); } } @@ -136,6 +138,8 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { throw new CloudRuntimeException("Unable to connect physical disk due to insufficient data"); } + volumePath = ScaleIOUtil.getVolumePath(volumePath); + int waitTimeInSec = DEFAULT_DISK_WAIT_TIME_IN_SECS; if (details != null && details.containsKey(StorageManager.STORAGE_POOL_DISK_WAIT.toString())) { String waitTime = details.get(StorageManager.STORAGE_POOL_DISK_WAIT.toString()); @@ -252,8 +256,8 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { } destDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW); - destDisk.setSize(disk.getVirtualSize()); - destDisk.setVirtualSize(disk.getSize()); + destDisk.setVirtualSize(disk.getVirtualSize()); + destDisk.setSize(disk.getSize()); QemuImg qemu = new QemuImg(timeout); QemuImgFile srcFile = null; diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePoolTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePoolTest.java index cb9ffaee531..4f18c38a164 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePoolTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePoolTest.java @@ -100,7 +100,7 @@ public class ScaleIOStoragePoolTest { } public void testGetPhysicalDiskWithWildcardFileFilter() throws Exception { - final String volumePath = "6c3362b500000001"; + final String volumePath = "6c3362b500000001:vol-139-3d2c-12f0"; final String systemId = "218ce1797566a00f"; File dir = PowerMockito.mock(File.class); @@ -108,7 +108,8 @@ public class ScaleIOStoragePoolTest { // TODO: Mock file in dir File[] files = new File[1]; - String diskFilePath = ScaleIOUtil.DISK_PATH + File.separator + ScaleIOUtil.DISK_NAME_PREFIX + systemId + "-" + volumePath; + String volumeId = ScaleIOUtil.getVolumePath(volumePath); + String diskFilePath = ScaleIOUtil.DISK_PATH + File.separator + ScaleIOUtil.DISK_NAME_PREFIX + systemId + "-" + volumeId; files[0] = new File(diskFilePath); PowerMockito.when(dir.listFiles(any(FileFilter.class))).thenReturn(files); @@ -118,10 +119,11 @@ public class ScaleIOStoragePoolTest { @Test public void testGetPhysicalDiskWithSystemId() throws Exception { - final String volumePath = "6c3362b500000001"; + final String volumePath = "6c3362b500000001:vol-139-3d2c-12f0"; + final String volumeId = ScaleIOUtil.getVolumePath(volumePath); final String systemId = "218ce1797566a00f"; PowerMockito.mockStatic(ScaleIOUtil.class); - when(ScaleIOUtil.getSystemIdForVolume(volumePath)).thenReturn(systemId); + when(ScaleIOUtil.getSystemIdForVolume(volumeId)).thenReturn(systemId); // TODO: Mock file exists File file = PowerMockito.mock(File.class); @@ -134,9 +136,10 @@ public class ScaleIOStoragePoolTest { @Test public void testConnectPhysicalDisk() { - final String volumePath = "6c3362b500000001"; + final String volumePath = "6c3362b500000001:vol-139-3d2c-12f0"; + final String volumeId = ScaleIOUtil.getVolumePath(volumePath); final String systemId = "218ce1797566a00f"; - final String diskFilePath = ScaleIOUtil.DISK_PATH + File.separator + ScaleIOUtil.DISK_NAME_PREFIX + systemId + "-" + volumePath; + final String diskFilePath = ScaleIOUtil.DISK_PATH + File.separator + ScaleIOUtil.DISK_NAME_PREFIX + systemId + "-" + volumeId; KVMPhysicalDisk disk = new KVMPhysicalDisk(diskFilePath, volumePath, pool); disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); disk.setSize(8192); @@ -144,7 +147,7 @@ public class ScaleIOStoragePoolTest { assertEquals(disk.getPath(), "/dev/disk/by-id/emc-vol-218ce1797566a00f-6c3362b500000001"); - when(adapter.getPhysicalDisk(volumePath, pool)).thenReturn(disk); + when(adapter.getPhysicalDisk(volumeId, pool)).thenReturn(disk); final boolean result = adapter.connectPhysicalDisk(volumePath, pool, null); assertTrue(result); diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTree.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTree.java new file mode 100644 index 00000000000..824a4c5496c --- /dev/null +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTree.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.storage.datastore.api; + +public class VTree { + String storagePoolId; + VTreeMigrationInfo vtreeMigrationInfo; + + public String getStoragePoolId() { + return storagePoolId; + } + + public void setStoragePoolId(String storagePoolId) { + this.storagePoolId = storagePoolId; + } + + public VTreeMigrationInfo getVTreeMigrationInfo() { + return vtreeMigrationInfo; + } + + public void setVTreeMigrationInfo(VTreeMigrationInfo vtreeMigrationInfo) { + this.vtreeMigrationInfo = vtreeMigrationInfo; + } +} diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTreeMigrationInfo.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTreeMigrationInfo.java new file mode 100644 index 00000000000..f4e926bfd33 --- /dev/null +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/api/VTreeMigrationInfo.java @@ -0,0 +1,76 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.storage.datastore.api; + +import com.cloud.utils.EnumUtils; + +public class VTreeMigrationInfo { + public enum MigrationStatus { + NotInMigration, + MigrationNormal, + PendingRetry, + InternalPausing, + GracefullyPausing, + ForcefullyPausing, + Paused, + PendingMigration, + PendingRebalance, + None + } + + String sourceStoragePoolId; + String destinationStoragePoolId; + MigrationStatus migrationStatus; + Long migrationQueuePosition; + + public String getSourceStoragePoolId() { + return sourceStoragePoolId; + } + + public void setSourceStoragePoolId(String sourceStoragePoolId) { + this.sourceStoragePoolId = sourceStoragePoolId; + } + + public String getDestinationStoragePoolId() { + return destinationStoragePoolId; + } + + public void setDestinationStoragePoolId(String destinationStoragePoolId) { + this.destinationStoragePoolId = destinationStoragePoolId; + } + + public MigrationStatus getMigrationStatus() { + return migrationStatus; + } + + public void setMigrationStatus(String migrationStatus) { + this.migrationStatus = EnumUtils.fromString(MigrationStatus.class, migrationStatus, MigrationStatus.None); + } + + public void setMigrationStatus(MigrationStatus migrationStatus) { + this.migrationStatus = migrationStatus; + } + + public Long getMigrationQueuePosition() { + return migrationQueuePosition; + } + + public void setMigrationQueuePosition(Long migrationQueuePosition) { + this.migrationQueuePosition = migrationQueuePosition; + } +} diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClient.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClient.java index 08f25254cfd..f6b10f88832 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClient.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClient.java @@ -51,9 +51,11 @@ public interface ScaleIOGatewayClient { List listSnapshotVolumes(); Volume getVolume(String volumeId); Volume getVolumeByName(String name); + boolean renameVolume(final String volumeId, final String newName); Volume resizeVolume(final String volumeId, final Integer sizeInGb); Volume cloneVolume(final String sourceVolumeId, final String destVolumeName); boolean deleteVolume(final String volumeId); + boolean migrateVolume(final String srcVolumeId, final String destPoolId, final int timeoutInSecs); boolean mapVolumeToSdc(final String volumeId, final String sdcId); boolean mapVolumeToSdcWithLimits(final String volumeId, final String sdcId, final Long iopsLimit, final Long bandwidthLimitInKbps); diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImpl.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImpl.java index c981300d81d..5e8568dede1 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImpl.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImpl.java @@ -44,6 +44,8 @@ import org.apache.cloudstack.storage.datastore.api.SnapshotDefs; import org.apache.cloudstack.storage.datastore.api.SnapshotGroup; import org.apache.cloudstack.storage.datastore.api.StoragePool; import org.apache.cloudstack.storage.datastore.api.StoragePoolStatistics; +import org.apache.cloudstack.storage.datastore.api.VTree; +import org.apache.cloudstack.storage.datastore.api.VTreeMigrationInfo; import org.apache.cloudstack.storage.datastore.api.Volume; import org.apache.cloudstack.storage.datastore.api.VolumeStatistics; import org.apache.cloudstack.utils.security.SSLUtils; @@ -361,6 +363,29 @@ public class ScaleIOGatewayClientImpl implements ScaleIOGatewayClient { return null; } + @Override + public boolean renameVolume(final String volumeId, final String newName) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(volumeId), "Volume id cannot be null"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(newName), "New name for volume cannot be null"); + + HttpResponse response = null; + try { + response = post( + "/instances/Volume::" + volumeId + "/action/setVolumeName", + String.format("{\"newName\":\"%s\"}", newName)); + checkResponseOK(response); + return true; + } catch (final IOException e) { + LOG.error("Failed to rename PowerFlex volume due to: ", e); + checkResponseTimeOut(e); + } finally { + if (response != null) { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + return false; + } + @Override public Volume resizeVolume(final String volumeId, final Integer sizeInGB) { Preconditions.checkArgument(!Strings.isNullOrEmpty(volumeId), "Volume id cannot be null"); @@ -755,6 +780,226 @@ public class ScaleIOGatewayClientImpl implements ScaleIOGatewayClient { return false; } + @Override + public boolean migrateVolume(final String srcVolumeId, final String destPoolId, final int timeoutInSecs) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(srcVolumeId), "src volume id cannot be null"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(destPoolId), "dest pool id cannot be null"); + Preconditions.checkArgument(timeoutInSecs > 0, "timeout must be greater than 0"); + + try { + Volume volume = getVolume(srcVolumeId); + if (volume == null || Strings.isNullOrEmpty(volume.getVtreeId())) { + LOG.warn("Couldn't find the volume(-tree), can not migrate the volume " + srcVolumeId); + return false; + } + + String srcPoolId = volume.getStoragePoolId(); + LOG.debug("Migrating the volume: " + srcVolumeId + " on the src pool: " + srcPoolId + " to the dest pool: " + destPoolId + + " in the same PowerFlex cluster"); + + HttpResponse response = null; + try { + response = post( + "/instances/Volume::" + srcVolumeId + "/action/migrateVTree", + String.format("{\"destSPId\":\"%s\"}", destPoolId)); + checkResponseOK(response); + } catch (final IOException e) { + LOG.error("Unable to migrate PowerFlex volume due to: ", e); + checkResponseTimeOut(e); + throw e; + } finally { + if (response != null) { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + + LOG.debug("Wait until the migration is complete for the volume: " + srcVolumeId); + long migrationStartTime = System.currentTimeMillis(); + boolean status = waitForVolumeMigrationToComplete(volume.getVtreeId(), timeoutInSecs); + + // Check volume storage pool and migration status + // volume, v-tree, snapshot ids remains same after the migration + volume = getVolume(srcVolumeId); + if (volume == null || volume.getStoragePoolId() == null) { + LOG.warn("Couldn't get the volume: " + srcVolumeId + " details after migration"); + return status; + } else { + String volumeOnPoolId = volume.getStoragePoolId(); + // confirm whether the volume is on the dest storage pool or not + if (status && destPoolId.equalsIgnoreCase(volumeOnPoolId)) { + LOG.debug("Migration success for the volume: " + srcVolumeId); + return true; + } else { + try { + // Check and pause any migration activity on the volume + status = false; + VTreeMigrationInfo.MigrationStatus migrationStatus = getVolumeTreeMigrationStatus(volume.getVtreeId()); + if (migrationStatus != null && migrationStatus != VTreeMigrationInfo.MigrationStatus.NotInMigration) { + long timeElapsedInSecs = (System.currentTimeMillis() - migrationStartTime) / 1000; + int timeRemainingInSecs = (int) (timeoutInSecs - timeElapsedInSecs); + if (timeRemainingInSecs > (timeoutInSecs / 2)) { + // Try to pause gracefully (continue the migration) if atleast half of the time is remaining + pauseVolumeMigration(srcVolumeId, false); + status = waitForVolumeMigrationToComplete(volume.getVtreeId(), timeRemainingInSecs); + } + } + + if (!status) { + rollbackVolumeMigration(srcVolumeId); + } + + return status; + } catch (Exception ex) { + LOG.warn("Exception on pause/rollback migration of the volume: " + srcVolumeId + " - " + ex.getLocalizedMessage()); + } + } + } + } catch (final Exception e) { + LOG.error("Failed to migrate PowerFlex volume due to: " + e.getMessage(), e); + throw new CloudRuntimeException("Failed to migrate PowerFlex volume due to: " + e.getMessage()); + } + + LOG.debug("Migration failed for the volume: " + srcVolumeId); + return false; + } + + private boolean waitForVolumeMigrationToComplete(final String volumeTreeId, int waitTimeoutInSecs) { + LOG.debug("Waiting for the migration to complete for the volume-tree " + volumeTreeId); + if (Strings.isNullOrEmpty(volumeTreeId)) { + LOG.warn("Invalid volume-tree id, unable to check the migration status of the volume-tree " + volumeTreeId); + return false; + } + + int delayTimeInSecs = 3; + while (waitTimeoutInSecs > 0) { + try { + // Wait and try after few secs (reduce no. of client API calls to check the migration status) and return after migration is complete + Thread.sleep(delayTimeInSecs * 1000); + + VTreeMigrationInfo.MigrationStatus migrationStatus = getVolumeTreeMigrationStatus(volumeTreeId); + if (migrationStatus != null && migrationStatus == VTreeMigrationInfo.MigrationStatus.NotInMigration) { + LOG.debug("Migration completed for the volume-tree " + volumeTreeId); + return true; + } + } catch (Exception ex) { + LOG.warn("Exception while checking for migration status of the volume-tree: " + volumeTreeId + " - " + ex.getLocalizedMessage()); + // don't do anything + } finally { + waitTimeoutInSecs = waitTimeoutInSecs - delayTimeInSecs; + } + } + + LOG.debug("Unable to complete the migration for the volume-tree " + volumeTreeId); + return false; + } + + private VTreeMigrationInfo.MigrationStatus getVolumeTreeMigrationStatus(final String volumeTreeId) { + if (Strings.isNullOrEmpty(volumeTreeId)) { + LOG.warn("Invalid volume-tree id, unable to get the migration status of the volume-tree " + volumeTreeId); + return null; + } + + HttpResponse response = null; + try { + response = get("/instances/VTree::" + volumeTreeId); + checkResponseOK(response); + ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + VTree volumeTree = mapper.readValue(response.getEntity().getContent(), VTree.class); + if (volumeTree != null && volumeTree.getVTreeMigrationInfo() != null) { + return volumeTree.getVTreeMigrationInfo().getMigrationStatus(); + } + } catch (final IOException e) { + LOG.error("Failed to migrate PowerFlex volume due to:", e); + checkResponseTimeOut(e); + } finally { + if (response != null) { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + return null; + } + + private boolean rollbackVolumeMigration(final String srcVolumeId) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(srcVolumeId), "src volume id cannot be null"); + + HttpResponse response = null; + try { + Volume volume = getVolume(srcVolumeId); + VTreeMigrationInfo.MigrationStatus migrationStatus = getVolumeTreeMigrationStatus(volume.getVtreeId()); + if (migrationStatus != null && migrationStatus == VTreeMigrationInfo.MigrationStatus.NotInMigration) { + LOG.debug("Volume: " + srcVolumeId + " is not migrating, no need to rollback"); + return true; + } + + pauseVolumeMigration(srcVolumeId, true); // Pause forcefully + // Wait few secs for volume migration to change to Paused state + boolean paused = false; + int retryCount = 3; + while (retryCount > 0) { + try { + Thread.sleep(3000); // Try after few secs + migrationStatus = getVolumeTreeMigrationStatus(volume.getVtreeId()); // Get updated migration status + if (migrationStatus != null && migrationStatus == VTreeMigrationInfo.MigrationStatus.Paused) { + LOG.debug("Migration for the volume: " + srcVolumeId + " paused"); + paused = true; + break; + } + } catch (Exception ex) { + LOG.warn("Exception while checking for migration pause status of the volume: " + srcVolumeId + " - " + ex.getLocalizedMessage()); + // don't do anything + } finally { + retryCount--; + } + } + + if (paused) { + // Rollback migration to the src pool (should be quick) + response = post( + "/instances/Volume::" + srcVolumeId + "/action/migrateVTree", + String.format("{\"destSPId\":\"%s\"}", volume.getStoragePoolId())); + checkResponseOK(response); + return true; + } else { + LOG.warn("Migration for the volume: " + srcVolumeId + " didn't pause, couldn't rollback"); + } + } catch (final IOException e) { + LOG.error("Failed to rollback volume migration due to: ", e); + checkResponseTimeOut(e); + } finally { + if (response != null) { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + return false; + } + + private boolean pauseVolumeMigration(final String volumeId, final boolean forced) { + if (Strings.isNullOrEmpty(volumeId)) { + LOG.warn("Invalid Volume Id, Unable to pause migration of the volume " + volumeId); + return false; + } + + HttpResponse response = null; + try { + // When paused gracefully, all data currently being moved is allowed to complete the migration. + // When paused forcefully, migration of unfinished data is aborted and data is left at the source, if possible. + // Pausing forcefully carries a potential risk to data. + response = post( + "/instances/Volume::" + volumeId + "/action/pauseVTreeMigration", + String.format("{\"pauseType\":\"%s\"}", forced ? "Forcefully" : "Gracefully")); + checkResponseOK(response); + return true; + } catch (final IOException e) { + LOG.error("Failed to pause migration of the volume due to: ", e); + checkResponseTimeOut(e); + } finally { + if (response != null) { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + return false; + } + /////////////////////////////////////////////////////// //////////////// StoragePool APIs ///////////////////// /////////////////////////////////////////////////////// diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java index c2300b9fa18..82078f115cb 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java @@ -36,6 +36,7 @@ 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.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.RemoteHostEndPoint; import org.apache.cloudstack.storage.command.CommandResult; import org.apache.cloudstack.storage.command.CopyCommand; @@ -48,10 +49,12 @@ import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClientConnec import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.util.ScaleIOUtil; import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.Answer; @@ -59,10 +62,12 @@ import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DataTO; import com.cloud.alert.AlertManager; +import com.cloud.configuration.Config; import com.cloud.host.Host; import com.cloud.server.ManagementServerImpl; import com.cloud.storage.DataStoreRole; import com.cloud.storage.ResizeVolumePayload; +import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; @@ -70,9 +75,11 @@ import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeDetailVO; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachineManager; @@ -97,7 +104,11 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @Inject private SnapshotDataStoreDao snapshotDataStoreDao; @Inject + protected SnapshotDao snapshotDao; + @Inject private AlertManager alertMgr; + @Inject + private ConfigurationDao configDao; public ScaleIOPrimaryDataStoreDriver() { @@ -140,7 +151,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { throw new CloudRuntimeException("Unable to grant access to volume: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); } - return client.mapVolumeToSdcWithLimits(volume.getPath(), sdc.getId(), iopsLimit, bandwidthLimitInKbps); + return client.mapVolumeToSdcWithLimits(ScaleIOUtil.getVolumePath(volume.getPath()), sdc.getId(), iopsLimit, bandwidthLimitInKbps); } else if (DataObjectType.TEMPLATE.equals(dataObject.getType())) { final VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(dataStore.getId(), dataObject.getId()); LOGGER.debug("Granting access for PowerFlex template volume: " + templatePoolRef.getInstallPath()); @@ -152,7 +163,19 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { throw new CloudRuntimeException("Unable to grant access to template: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); } - return client.mapVolumeToSdc(templatePoolRef.getInstallPath(), sdc.getId()); + return client.mapVolumeToSdc(ScaleIOUtil.getVolumePath(templatePoolRef.getInstallPath()), sdc.getId()); + } else if (DataObjectType.SNAPSHOT.equals(dataObject.getType())) { + SnapshotInfo snapshot = (SnapshotInfo) dataObject; + LOGGER.debug("Granting access for PowerFlex volume snapshot: " + snapshot.getPath()); + + final ScaleIOGatewayClient client = getScaleIOClient(dataStore.getId()); + final Sdc sdc = client.getConnectedSdcByIp(host.getPrivateIpAddress()); + if (sdc == null) { + alertHostSdcDisconnection(host); + throw new CloudRuntimeException("Unable to grant access to snapshot: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); + } + + return client.mapVolumeToSdc(ScaleIOUtil.getVolumePath(snapshot.getPath()), sdc.getId()); } return false; @@ -174,7 +197,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { throw new CloudRuntimeException("Unable to revoke access for volume: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); } - client.unmapVolumeFromSdc(volume.getPath(), sdc.getId()); + client.unmapVolumeFromSdc(ScaleIOUtil.getVolumePath(volume.getPath()), sdc.getId()); } else if (DataObjectType.TEMPLATE.equals(dataObject.getType())) { final VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(dataStore.getId(), dataObject.getId()); LOGGER.debug("Revoking access for PowerFlex template volume: " + templatePoolRef.getInstallPath()); @@ -185,7 +208,18 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { throw new CloudRuntimeException("Unable to revoke access for template: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); } - client.unmapVolumeFromSdc(templatePoolRef.getInstallPath(), sdc.getId()); + client.unmapVolumeFromSdc(ScaleIOUtil.getVolumePath(templatePoolRef.getInstallPath()), sdc.getId()); + } else if (DataObjectType.SNAPSHOT.equals(dataObject.getType())) { + SnapshotInfo snapshot = (SnapshotInfo) dataObject; + LOGGER.debug("Revoking access for PowerFlex volume snapshot: " + snapshot.getPath()); + + final ScaleIOGatewayClient client = getScaleIOClient(dataStore.getId()); + final Sdc sdc = client.getConnectedSdcByIp(host.getPrivateIpAddress()); + if (sdc == null) { + throw new CloudRuntimeException("Unable to revoke access for snapshot: " + dataObject.getId() + ", no Sdc connected with host ip: " + host.getPrivateIpAddress()); + } + + client.unmapVolumeFromSdc(ScaleIOUtil.getVolumePath(snapshot.getPath()), sdc.getId()); } } catch (Exception e) { LOGGER.warn("Failed to revoke access due to: " + e.getMessage(), e); @@ -300,7 +334,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO)snapshotInfo.getTO(); final ScaleIOGatewayClient client = getScaleIOClient(storagePoolId); - final String scaleIOVolumeId = volumeVO.getPath(); + final String scaleIOVolumeId = ScaleIOUtil.getVolumePath(volumeVO.getPath()); String snapshotName = String.format("%s-%s-%s-%s", ScaleIOUtil.SNAPSHOT_PREFIX, snapshotInfo.getId(), storagePool.getUuid().split("-")[0].substring(4), ManagementServerImpl.customCsIdentifier.value()); @@ -311,7 +345,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { throw new CloudRuntimeException("Failed to take snapshot on PowerFlex cluster"); } - snapshotObjectTo.setPath(scaleIOVolume.getId()); + snapshotObjectTo.setPath(ScaleIOUtil.updatedPathWithVolumeName(scaleIOVolume.getId(), snapshotName)); CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo); result = new CreateCmdResult(null, createObjectAnswer); result.setResult(null); @@ -347,8 +381,8 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { long storagePoolId = volumeVO.getPoolId(); final ScaleIOGatewayClient client = getScaleIOClient(storagePoolId); - String snapshotVolumeId = snapshot.getPath(); - final String destVolumeId = volumeVO.getPath(); + String snapshotVolumeId = ScaleIOUtil.getVolumePath(snapshot.getPath()); + final String destVolumeId = ScaleIOUtil.getVolumePath(volumeVO.getPath()); client.revertSnapshot(snapshotVolumeId, destVolumeId); CommandResult commandResult = new CommandResult(); @@ -384,8 +418,9 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } VolumeVO volume = volumeDao.findById(volumeInfo.getId()); - volume.set_iScsiName(scaleIOVolume.getId()); - volume.setPath(scaleIOVolume.getId()); + String volumePath = ScaleIOUtil.updatedPathWithVolumeName(scaleIOVolume.getId(), scaleIOVolumeName); + volume.set_iScsiName(volumePath); + volume.setPath(volumePath); volume.setFolder(scaleIOVolume.getVtreeId()); volume.setSize(scaleIOVolume.getSizeInKb() * 1024); volume.setPoolType(Storage.StoragePoolType.PowerFlex); @@ -399,7 +434,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { storagePool.setUsedBytes(usedBytes > capacityBytes ? capacityBytes : usedBytes); storagePoolDao.update(storagePoolId, storagePool); - return volume.getPath(); + return volumePath; } catch (Exception e) { String errMsg = "Unable to create PowerFlex Volume due to " + e.getMessage(); LOGGER.warn(errMsg); @@ -431,7 +466,8 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(storagePoolId, templateInfo.getId()); - templatePoolRef.setInstallPath(scaleIOVolume.getId()); + String templatePath = ScaleIOUtil.updatedPathWithVolumeName(scaleIOVolume.getId(), scaleIOVolumeName); + templatePoolRef.setInstallPath(templatePath); templatePoolRef.setLocalDownloadPath(scaleIOVolume.getId()); templatePoolRef.setTemplateSize(scaleIOVolume.getSizeInKb() * 1024); vmTemplatePoolDao.update(templatePoolRef.getId(), templatePoolRef); @@ -442,7 +478,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { storagePool.setUsedBytes(usedBytes > capacityBytes ? capacityBytes : usedBytes); storagePoolDao.update(storagePoolId, storagePool); - return scaleIOVolume.getId(); + return templatePath; } catch (Exception e) { String errMsg = "Unable to create PowerFlex template volume due to " + e.getMessage(); LOGGER.warn(errMsg); @@ -452,15 +488,15 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @Override public void createAsync(DataStore dataStore, DataObject dataObject, AsyncCompletionCallback callback) { - String scaleIOVolId = null; + String scaleIOVolumePath = null; String errMsg = null; try { if (dataObject.getType() == DataObjectType.VOLUME) { LOGGER.debug("createAsync - creating volume"); - scaleIOVolId = createVolume((VolumeInfo) dataObject, dataStore.getId()); + scaleIOVolumePath = createVolume((VolumeInfo) dataObject, dataStore.getId()); } else if (dataObject.getType() == DataObjectType.TEMPLATE) { LOGGER.debug("createAsync - creating template"); - scaleIOVolId = createTemplateVolume((TemplateInfo)dataObject, dataStore.getId()); + scaleIOVolumePath = createTemplateVolume((TemplateInfo)dataObject, dataStore.getId()); } else { errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to createAsync"; LOGGER.error(errMsg); @@ -474,7 +510,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } if (callback != null) { - CreateCmdResult result = new CreateCmdResult(scaleIOVolId, new Answer(null, errMsg == null, errMsg)); + CreateCmdResult result = new CreateCmdResult(scaleIOVolumePath, new Answer(null, errMsg == null, errMsg)); result.setResult(errMsg); callback.complete(result); } @@ -490,23 +526,26 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { Preconditions.checkArgument(storagePool != null && storagePool.getHostAddress() != null, "storagePool and host address should not be null"); String errMsg = null; - String scaleIOVolumeId = null; + String scaleIOVolumePath = null; try { boolean deleteResult = false; if (dataObject.getType() == DataObjectType.VOLUME) { LOGGER.debug("deleteAsync - deleting volume"); - scaleIOVolumeId = ((VolumeInfo) dataObject).getPath(); + scaleIOVolumePath = ((VolumeInfo) dataObject).getPath(); } else if (dataObject.getType() == DataObjectType.SNAPSHOT) { LOGGER.debug("deleteAsync - deleting snapshot"); - scaleIOVolumeId = ((SnapshotInfo) dataObject).getPath(); + scaleIOVolumePath = ((SnapshotInfo) dataObject).getPath(); } else if (dataObject.getType() == DataObjectType.TEMPLATE) { LOGGER.debug("deleteAsync - deleting template"); - scaleIOVolumeId = ((TemplateInfo) dataObject).getInstallPath(); + scaleIOVolumePath = ((TemplateInfo) dataObject).getInstallPath(); } else { errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to deleteAsync"; + LOGGER.error(errMsg); + throw new CloudRuntimeException(errMsg); } try { + String scaleIOVolumeId = ScaleIOUtil.getVolumePath(scaleIOVolumePath); final ScaleIOGatewayClient client = getScaleIOClient(storagePoolId); deleteResult = client.deleteVolume(scaleIOVolumeId); if (!deleteResult) { @@ -518,7 +557,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { storagePool.setUsedBytes(usedBytes < 0 ? 0 : usedBytes); storagePoolDao.update(storagePoolId, storagePool); } catch (Exception e) { - errMsg = "Unable to delete PowerFlex volume: " + scaleIOVolumeId + " due to " + e.getMessage(); + errMsg = "Unable to delete PowerFlex volume: " + scaleIOVolumePath + " due to " + e.getMessage(); LOGGER.warn(errMsg); throw new CloudRuntimeException(errMsg, e); } @@ -544,46 +583,207 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @Override public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) { - DataStore srcStore = destData.getDataStore(); - DataStore destStore = destData.getDataStore(); - if (srcStore.getRole() == DataStoreRole.Primary && srcData.getType() == DataObjectType.TEMPLATE - && (destStore.getRole() == DataStoreRole.Primary && destData.getType() == DataObjectType.VOLUME)) { - int primaryStorageDownloadWait = StorageManager.PRIMARY_STORAGE_DOWNLOAD_WAIT.value(); - Answer answer = null; - String errMsg = null; + Answer answer = null; + String errMsg = null; - try { - LOGGER.debug("Initiating copy from PowerFlex template volume on host " + destHost != null ? destHost.getId() : ""); - CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); + try { + DataStore srcStore = srcData.getDataStore(); + DataStore destStore = destData.getDataStore(); + if (srcStore.getRole() == DataStoreRole.Primary && (destStore.getRole() == DataStoreRole.Primary && destData.getType() == DataObjectType.VOLUME)) { + if (srcData.getType() == DataObjectType.TEMPLATE) { + answer = copyTemplateToVolume(srcData, destData, destHost); + if (answer == null) { + errMsg = "No answer for copying template to PowerFlex volume"; + } else if (!answer.getResult()) { + errMsg = answer.getDetails(); + } + } else if (srcData.getType() == DataObjectType.VOLUME) { + if (isSameScaleIOStorageInstance(srcStore, destStore)) { + answer = migrateVolume(srcData, destData); + } else { + answer = copyVolume(srcData, destData, destHost); + } - EndPoint ep = destHost != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(destHost) : selector.select(srcData.getDataStore()); - if (ep == null) { - errMsg = "No remote endpoint to send command, check if host or ssvm is down?"; - LOGGER.error(errMsg); - answer = new Answer(cmd, false, errMsg); + if (answer == null) { + errMsg = "No answer for migrate PowerFlex volume"; + } else if (!answer.getResult()) { + errMsg = answer.getDetails(); + } } else { - answer = ep.sendMessage(cmd); + errMsg = "Unsupported copy operation from src object: (" + srcData.getType() + ", " + srcData.getDataStore() + "), dest object: (" + + destData.getType() + ", " + destData.getDataStore() + ")"; + LOGGER.warn(errMsg); } - - if (answer != null && !answer.getResult()) { - errMsg = answer.getDetails(); - } - } catch (Exception e) { - LOGGER.debug("Failed to copy due to ", e); - errMsg = e.toString(); + } else { + errMsg = "Unsupported copy operation"; } - - CopyCommandResult result = new CopyCommandResult(null, answer); - result.setResult(errMsg); - callback.complete(result); + } catch (Exception e) { + LOGGER.debug("Failed to copy due to " + e.getMessage(), e); + errMsg = e.toString(); } + + CopyCommandResult result = new CopyCommandResult(null, answer); + result.setResult(errMsg); + callback.complete(result); + } + + private Answer copyTemplateToVolume(DataObject srcData, DataObject destData, Host destHost) { + // Copy PowerFlex/ScaleIO template to volume + LOGGER.debug("Initiating copy from PowerFlex template volume on host " + destHost != null ? destHost.getId() : ""); + int primaryStorageDownloadWait = StorageManager.PRIMARY_STORAGE_DOWNLOAD_WAIT.value(); + CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); + + Answer answer = null; + EndPoint ep = destHost != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(destHost) : selector.select(srcData.getDataStore()); + if (ep == null) { + String errorMsg = "No remote endpoint to send command, check if host or ssvm is down?"; + LOGGER.error(errorMsg); + answer = new Answer(cmd, false, errorMsg); + } else { + answer = ep.sendMessage(cmd); + } + + return answer; + } + + private Answer copyVolume(DataObject srcData, DataObject destData, Host destHost) { + // Copy PowerFlex/ScaleIO volume + LOGGER.debug("Initiating copy from PowerFlex volume on host " + destHost != null ? destHost.getId() : ""); + String value = configDao.getValue(Config.CopyVolumeWait.key()); + int copyVolumeWait = NumbersUtil.parseInt(value, Integer.parseInt(Config.CopyVolumeWait.getDefaultValue())); + + CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), copyVolumeWait, VirtualMachineManager.ExecuteInSequence.value()); + + Answer answer = null; + EndPoint ep = destHost != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(destHost) : selector.select(srcData.getDataStore()); + if (ep == null) { + String errorMsg = "No remote endpoint to send command, check if host or ssvm is down?"; + LOGGER.error(errorMsg); + answer = new Answer(cmd, false, errorMsg); + } else { + answer = ep.sendMessage(cmd); + } + + return answer; + } + + private Answer migrateVolume(DataObject srcData, DataObject destData) { + // Volume migration within same PowerFlex/ScaleIO cluster (with same System ID) + DataStore srcStore = srcData.getDataStore(); + DataStore destStore = destData.getDataStore(); + Answer answer = null; + try { + long srcPoolId = srcStore.getId(); + long destPoolId = destStore.getId(); + + final ScaleIOGatewayClient client = getScaleIOClient(srcPoolId); + final String srcVolumePath = ((VolumeInfo) srcData).getPath(); + final String srcVolumeId = ScaleIOUtil.getVolumePath(srcVolumePath); + final StoragePoolVO destStoragePool = storagePoolDao.findById(destPoolId); + final String destStoragePoolId = destStoragePool.getPath(); + int migrationTimeout = StorageManager.KvmStorageOfflineMigrationWait.value(); + boolean migrateStatus = client.migrateVolume(srcVolumeId, destStoragePoolId, migrationTimeout); + if (migrateStatus) { + String newVolumeName = String.format("%s-%s-%s-%s", ScaleIOUtil.VOLUME_PREFIX, destData.getId(), + destStoragePool.getUuid().split("-")[0].substring(4), ManagementServerImpl.customCsIdentifier.value()); + boolean renamed = client.renameVolume(srcVolumeId, newVolumeName); + + if (srcData.getId() != destData.getId()) { + VolumeVO destVolume = volumeDao.findById(destData.getId()); + // Volume Id in the PowerFlex/ScaleIO pool remains the same after the migration + // Update PowerFlex volume name only after it is renamed, to maintain the consistency + if (renamed) { + String newVolumePath = ScaleIOUtil.updatedPathWithVolumeName(srcVolumeId, newVolumeName); + destVolume.set_iScsiName(newVolumePath); + destVolume.setPath(newVolumePath); + } else { + destVolume.set_iScsiName(srcVolumePath); + destVolume.setPath(srcVolumePath); + } + volumeDao.update(destData.getId(), destVolume); + + VolumeVO srcVolume = volumeDao.findById(srcData.getId()); + srcVolume.set_iScsiName(null); + srcVolume.setPath(null); + srcVolume.setFolder(null); + volumeDao.update(srcData.getId(), srcVolume); + } else { + // Live migrate volume + VolumeVO volume = volumeDao.findById(srcData.getId()); + Long oldPoolId = volume.getPoolId(); + volume.setPoolId(destPoolId); + volume.setLastPoolId(oldPoolId); + volumeDao.update(srcData.getId(), volume); + } + + List snapshots = snapshotDao.listByVolumeId(srcData.getId()); + if (CollectionUtils.isNotEmpty(snapshots)) { + for (SnapshotVO snapshot : snapshots) { + SnapshotDataStoreVO snapshotStore = snapshotDataStoreDao.findBySnapshot(snapshot.getId(), DataStoreRole.Primary); + if (snapshotStore == null) { + continue; + } + + String snapshotVolumeId = ScaleIOUtil.getVolumePath(snapshotStore.getInstallPath()); + String newSnapshotName = String.format("%s-%s-%s-%s", ScaleIOUtil.SNAPSHOT_PREFIX, snapshot.getId(), + destStoragePool.getUuid().split("-")[0].substring(4), ManagementServerImpl.customCsIdentifier.value()); + renamed = client.renameVolume(snapshotVolumeId, newSnapshotName); + + snapshotStore.setDataStoreId(destPoolId); + // Snapshot Id in the PowerFlex/ScaleIO pool remains the same after the migration + // Update PowerFlex snapshot name only after it is renamed, to maintain the consistency + if (renamed) { + snapshotStore.setInstallPath(ScaleIOUtil.updatedPathWithVolumeName(snapshotVolumeId, newSnapshotName)); + } + snapshotDataStoreDao.update(snapshotStore.getId(), snapshotStore); + } + } + + answer = new Answer(null, true, null); + } else { + String errorMsg = "Failed to migrate PowerFlex volume: " + srcData.getId() + " to storage pool " + destPoolId; + LOGGER.debug(errorMsg); + answer = new Answer(null, false, errorMsg); + } + } catch (Exception e) { + LOGGER.error("Failed to migrate PowerFlex volume: " + srcData.getId() + " due to: " + e.getMessage()); + answer = new Answer(null, false, e.getMessage()); + } + + return answer; + } + + private boolean isSameScaleIOStorageInstance(DataStore srcStore, DataStore destStore) { + long srcPoolId = srcStore.getId(); + String srcPoolSystemId = null; + StoragePoolDetailVO srcPoolSystemIdDetail = storagePoolDetailsDao.findDetail(srcPoolId, ScaleIOGatewayClient.STORAGE_POOL_SYSTEM_ID); + if (srcPoolSystemIdDetail != null) { + srcPoolSystemId = srcPoolSystemIdDetail.getValue(); + } + + long destPoolId = destStore.getId(); + String destPoolSystemId = null; + StoragePoolDetailVO destPoolSystemIdDetail = storagePoolDetailsDao.findDetail(destPoolId, ScaleIOGatewayClient.STORAGE_POOL_SYSTEM_ID); + if (destPoolSystemIdDetail != null) { + destPoolSystemId = destPoolSystemIdDetail.getValue(); + } + + if (Strings.isNullOrEmpty(srcPoolSystemId) || Strings.isNullOrEmpty(destPoolSystemId)) { + throw new CloudRuntimeException("Failed to validate PowerFlex pools compatibility for migration as storage instance details are not available"); + } + + if (srcPoolSystemId.equals(destPoolSystemId)) { + return true; + } + + return false; } @Override public boolean canCopy(DataObject srcData, DataObject destData) { DataStore srcStore = destData.getDataStore(); DataStore destStore = destData.getDataStore(); - if (srcStore.getRole() == DataStoreRole.Primary && srcData.getType() == DataObjectType.TEMPLATE + if ((srcStore.getRole() == DataStoreRole.Primary && (srcData.getType() == DataObjectType.TEMPLATE || srcData.getType() == DataObjectType.VOLUME)) && (destStore.getRole() == DataStoreRole.Primary && destData.getType() == DataObjectType.VOLUME)) { StoragePoolVO srcPoolVO = storagePoolDao.findById(srcStore.getId()); StoragePoolVO destPoolVO = storagePoolDao.findById(destStore.getId()); @@ -601,7 +801,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { Preconditions.checkArgument(volumeInfo != null, "volumeInfo cannot be null"); try { - String scaleIOVolumeId = volumeInfo.getPath(); + String scaleIOVolumeId = ScaleIOUtil.getVolumePath(volumeInfo.getPath()); Long storagePoolId = volumeInfo.getPoolId(); ResizeVolumePayload payload = (ResizeVolumePayload)volumeInfo.getpayload(); @@ -642,11 +842,11 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @Override public void resize(DataObject dataObject, AsyncCompletionCallback callback) { - String scaleIOVolumeId = null; + String scaleIOVolumePath = null; String errMsg = null; try { if (dataObject.getType() == DataObjectType.VOLUME) { - scaleIOVolumeId = ((VolumeInfo) dataObject).getPath(); + scaleIOVolumePath = ((VolumeInfo) dataObject).getPath(); resizeVolume((VolumeInfo) dataObject); } else { errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to resize"; @@ -660,7 +860,7 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } if (callback != null) { - CreateCmdResult result = new CreateCmdResult(scaleIOVolumeId, new Answer(null, errMsg == null, errMsg)); + CreateCmdResult result = new CreateCmdResult(scaleIOVolumePath, new Answer(null, errMsg == null, errMsg)); result.setResult(errMsg); callback.complete(result); } @@ -702,20 +902,20 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } @Override - public Pair getVolumeStats(StoragePool storagePool, String volumeId) { + public Pair getVolumeStats(StoragePool storagePool, String volumePath) { Preconditions.checkArgument(storagePool != null, "storagePool cannot be null"); - Preconditions.checkArgument(!Strings.isNullOrEmpty(volumeId), "volumeId cannot be null"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(volumePath), "volumePath cannot be null"); try { final ScaleIOGatewayClient client = getScaleIOClient(storagePool.getId()); - VolumeStatistics volumeStatistics = client.getVolumeStatistics(volumeId); + VolumeStatistics volumeStatistics = client.getVolumeStatistics(ScaleIOUtil.getVolumePath(volumePath)); if (volumeStatistics != null) { Long provisionedSizeInBytes = volumeStatistics.getNetProvisionedAddressesInBytes(); Long allocatedSizeInBytes = volumeStatistics.getAllocatedSizeInBytes(); return new Pair(provisionedSizeInBytes, allocatedSizeInBytes); } } catch (Exception e) { - String errMsg = "Unable to get stats for the volume: " + volumeId + " in the pool: " + storagePool.getId() + " due to " + e.getMessage(); + String errMsg = "Unable to get stats for the volume: " + volumePath + " in the pool: " + storagePool.getId() + " due to " + e.getMessage(); LOGGER.warn(errMsg); throw new CloudRuntimeException(errMsg, e); } diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycle.java index 799590675f8..5c9ddea4752 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycle.java @@ -131,6 +131,8 @@ public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCyc public DataStore initialize(Map dsInfos) { String url = (String) dsInfos.get("url"); Long zoneId = (Long) dsInfos.get("zoneId"); + Long podId = (Long)dsInfos.get("podId"); + Long clusterId = (Long)dsInfos.get("clusterId"); String dataStoreName = (String) dsInfos.get("name"); String providerName = (String) dsInfos.get("providerName"); Long capacityBytes = (Long)dsInfos.get("capacityBytes"); @@ -138,6 +140,28 @@ public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCyc String tags = (String)dsInfos.get("tags"); Map details = (Map) dsInfos.get("details"); + if (zoneId == null) { + throw new CloudRuntimeException("Zone Id must be specified."); + } + + PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters(); + if (clusterId != null) { + // Primary datastore is cluster-wide, check and set the podId and clusterId parameters + if (podId == null) { + throw new CloudRuntimeException("Pod Id must also be specified when the Cluster Id is specified for Cluster-wide primary storage."); + } + + Hypervisor.HypervisorType hypervisorType = getHypervisorTypeForCluster(clusterId); + if (!isSupportedHypervisorType(hypervisorType)) { + throw new CloudRuntimeException("Unsupported hypervisor type: " + hypervisorType.toString()); + } + + parameters.setPodId(podId); + parameters.setClusterId(clusterId); + } else if (podId != null) { + throw new CloudRuntimeException("Cluster Id must also be specified when the Pod Id is specified for Cluster-wide primary storage."); + } + URI uri = null; try { uri = new URI(UriUtils.encodeURIComponent(url)); @@ -188,7 +212,6 @@ public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCyc final org.apache.cloudstack.storage.datastore.api.StoragePool scaleIOPool = this.findStoragePool(gatewayApiURL, gatewayUsername, gatewayPassword, storagePoolName); - PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters(); parameters.setZoneId(zoneId); parameters.setName(dataStoreName); parameters.setProviderName(providerName); @@ -414,6 +437,15 @@ public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCyc } } + private Hypervisor.HypervisorType getHypervisorTypeForCluster(long clusterId) { + ClusterVO cluster = clusterDao.findById(clusterId); + if (cluster == null) { + throw new CloudRuntimeException("Unable to locate the specified cluster: " + clusterId); + } + + return cluster.getHypervisorType(); + } + private static boolean isSupportedHypervisorType(Hypervisor.HypervisorType hypervisorType) { return Hypervisor.HypervisorType.KVM.equals(hypervisorType); } diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/util/ScaleIOUtil.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/util/ScaleIOUtil.java index d28d72c51ca..0180f17cdd7 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/util/ScaleIOUtil.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/util/ScaleIOUtil.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.storage.datastore.util; import org.apache.log4j.Logger; import com.cloud.utils.script.Script; +import com.google.common.base.Strings; public class ScaleIOUtil { private static final Logger LOGGER = Logger.getLogger(ScaleIOUtil.class); @@ -95,4 +96,24 @@ public class ScaleIOUtil { return result; } + + public static final String getVolumePath(String volumePathWithName) { + if (Strings.isNullOrEmpty(volumePathWithName)) { + return volumePathWithName; + } + + if (volumePathWithName.contains(":")) { + return volumePathWithName.substring(0, volumePathWithName.indexOf(':')); + } + + return volumePathWithName; + } + + public static final String updatedPathWithVolumeName(String volumePath, String volumeName) { + if (Strings.isNullOrEmpty(volumePath) || Strings.isNullOrEmpty(volumeName)) { + return volumePath; + } + + return String.format("%s:%s", volumePath, volumeName); + } } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index a81c39aa1ae..7ce74696b82 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -1792,6 +1792,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (caps != null) { boolean quiescevm = Boolean.parseBoolean(caps.get(DataStoreCapabilities.VOLUME_SNAPSHOT_QUIESCEVM.toString())); vr.setNeedQuiescevm(quiescevm); + + boolean supportsStorageSnapshot = Boolean.parseBoolean(caps.get(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString())); + vr.setSupportsStorageSnapshot(supportsStorageSnapshot); } } response.setResponses(volumeResponses, result.second()); diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index e674fff3910..7a7f78735c2 100755 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -2833,7 +2833,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati return _serviceOfferingDetailsDao.findZoneIds(serviceOfferingId); } - protected DiskOfferingVO createDiskOffering(final Long userId, final List domainIds, final List zoneIds, final String name, final String description, final String provisioningType, + protected DiskOfferingVO createDiskOffering(final Long userId, final List domainIds, final List zoneIds, final String name, final String description, final String provisioningType, final Long numGibibytes, String tags, boolean isCustomized, final boolean localStorageRequired, final boolean isDisplayOfferingEnabled, final Boolean isCustomizedIops, Long minIops, Long maxIops, Long bytesReadRate, Long bytesReadRateMax, Long bytesReadRateMaxLength, diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 09250482483..c247e2802fc 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -507,7 +507,7 @@ Configurable, StateListenerWe also all storage available filtering by data center, pod and cluster as the current storage pool used by the given volume. * */ - private List getAllStoragePoolCompatileWithVolumeSourceStoragePool(StoragePool srcVolumePool) { + private List getAllStoragePoolsCompatibleWithVolumeSourceStoragePool(StoragePool srcVolumePool) { List storagePools = new ArrayList<>(); - List zoneWideStoragePools = _poolDao.findZoneWideStoragePoolsByTags(srcVolumePool.getDataCenterId(), null); - if (CollectionUtils.isNotEmpty(zoneWideStoragePools)) { - storagePools.addAll(zoneWideStoragePools); - } - List clusterAndLocalStoragePools = _poolDao.listBy(srcVolumePool.getDataCenterId(), srcVolumePool.getPodId(), srcVolumePool.getClusterId(), null); - if (CollectionUtils.isNotEmpty(clusterAndLocalStoragePools)) { - storagePools.addAll(clusterAndLocalStoragePools); + // Storage pool with Zone Scope holds valid DataCenter Id only, Pod Id and Cluster Id are null + // Storage pool with Cluster/Host Scope holds valid DataCenter Id, Pod Id and Cluster Id + // Below methods call returns all the compatible pools with scope : ZONE, CLUSTER, HOST (as they are listed with Scope: null here) + List compatibleStoragePools = _poolDao.listBy(srcVolumePool.getDataCenterId(), srcVolumePool.getPodId(), srcVolumePool.getClusterId(), null); + if (CollectionUtils.isNotEmpty(compatibleStoragePools)) { + compatibleStoragePools.remove(srcVolumePool); + storagePools.addAll(compatibleStoragePools); } + return storagePools; } @@ -1540,7 +1541,6 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (isLocalPoolSameHostAsSourcePool || pool.isShared()) { suitablePools.add(pool); } - } } return suitablePools; @@ -2391,6 +2391,11 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return _consoleProxyDao.findById(instanceId); } + private ConsoleProxyVO forceRebootConsoleProxy(final VMInstanceVO systemVm) throws ResourceUnavailableException, OperationTimedoutException, ConcurrentOperationException { + _itMgr.advanceStop(systemVm.getUuid(), false); + return _consoleProxyMgr.startProxy(systemVm.getId(), true); + } + protected ConsoleProxyVO destroyConsoleProxy(final long instanceId) { final ConsoleProxyVO proxy = _consoleProxyDao.findById(instanceId); @@ -3262,6 +3267,11 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return _secStorageVmDao.findById(instanceId); } + private SecondaryStorageVmVO forceRebootSecondaryStorageVm(final VMInstanceVO systemVm) throws ResourceUnavailableException, OperationTimedoutException, ConcurrentOperationException { + _itMgr.advanceStop(systemVm.getUuid(), false); + return _secStorageVmMgr.startSecStorageVm(systemVm.getId()); + } + protected SecondaryStorageVmVO destroySecondaryStorageVm(final long instanceId) { final SecondaryStorageVmVO secStorageVm = _secStorageVmDao.findById(instanceId); if (_secStorageVmMgr.destroySecStorageVm(instanceId)) { @@ -3416,12 +3426,24 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe throw ex; } - if (systemVm.getType().equals(VirtualMachine.Type.ConsoleProxy)) { - ActionEventUtils.startNestedActionEvent(EventTypes.EVENT_PROXY_REBOOT, "rebooting console proxy Vm"); - return rebootConsoleProxy(cmd.getId()); - } else { - ActionEventUtils.startNestedActionEvent(EventTypes.EVENT_SSVM_REBOOT, "rebooting secondary storage Vm"); - return rebootSecondaryStorageVm(cmd.getId()); + try { + if (systemVm.getType().equals(VirtualMachine.Type.ConsoleProxy)) { + ActionEventUtils.startNestedActionEvent(EventTypes.EVENT_PROXY_REBOOT, "rebooting console proxy Vm"); + if (cmd.isForced()) { + return forceRebootConsoleProxy(systemVm); + } + return rebootConsoleProxy(cmd.getId()); + } else { + ActionEventUtils.startNestedActionEvent(EventTypes.EVENT_SSVM_REBOOT, "rebooting secondary storage Vm"); + if (cmd.isForced()) { + return forceRebootSecondaryStorageVm(systemVm); + } + return rebootSecondaryStorageVm(cmd.getId()); + } + } catch (final ResourceUnavailableException e) { + throw new CloudRuntimeException("Unable to reboot " + systemVm, e); + } catch (final OperationTimedoutException e) { + throw new CloudRuntimeException("Operation timed out - Unable to reboot " + systemVm, e); } } diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 5559877b2e3..2619a4acafe 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -1710,6 +1710,38 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C } } + @Override + public Host findUpAndEnabledHostWithAccessToStoragePools(List poolIds) { + List hostIds = _storagePoolHostDao.findHostsConnectedToPools(poolIds); + if (hostIds.isEmpty()) { + return null; + } + + for (Long hostId : hostIds) { + Host host = _hostDao.findById(hostId); + if (canHostAccessStoragePools(host, poolIds)) { + return host; + } + } + + return null; + } + + private boolean canHostAccessStoragePools(Host host, List poolIds) { + if (poolIds == null || poolIds.isEmpty()) { + return false; + } + + for (Long poolId : poolIds) { + StoragePool pool = _storagePoolDao.findById(poolId); + if (!canHostAccessStoragePool(host, pool)) { + return false; + } + } + + return true; + } + @Override @DB public List findStoragePoolsConnectedToHost(long hostId) { @@ -2095,6 +2127,36 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C return tmpl.getSize(); } + @Override + public boolean storagePoolCompatibleWithVolumePool(StoragePool pool, Volume volume) { + if (pool == null || volume == null) { + return false; + } + + if (volume.getPoolId() == null) { + // Volume is not allocated to any pool. Not possible to check compatibility with other pool, let it try + return true; + } + + StoragePool volumePool = _storagePoolDao.findById(volume.getPoolId()); + if (volumePool == null) { + // Volume pool doesn't exist. Not possible to check compatibility with other pool, let it try + return true; + } + + if (volume.getState() == Volume.State.Ready) { + if (volumePool.getPoolType() == Storage.StoragePoolType.PowerFlex && pool.getPoolType() != Storage.StoragePoolType.PowerFlex) { + return false; + } else if (volumePool.getPoolType() != Storage.StoragePoolType.PowerFlex && pool.getPoolType() == Storage.StoragePoolType.PowerFlex) { + return false; + } + } else { + return false; + } + + return true; + } + @Override public void createCapacityEntry(long poolId) { StoragePoolVO storage = _storagePoolDao.findById(poolId); diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index d1ddb520953..db680851501 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -74,6 +74,8 @@ import org.apache.cloudstack.storage.command.AttachCommand; import org.apache.cloudstack.storage.command.DettachCommand; import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; @@ -213,6 +215,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @Inject private SnapshotDao _snapshotDao; @Inject + private SnapshotDataStoreDao _snapshotDataStoreDao; + @Inject private ServiceOfferingDetailsDao _serviceOfferingDetailsDao; @Inject private UserVmDao _userVmDao; @@ -689,6 +693,15 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (snapshotCheck.getState() != Snapshot.State.BackedUp) { throw new InvalidParameterValueException("Snapshot id=" + snapshotId + " is not in " + Snapshot.State.BackedUp + " state yet and can't be used for volume creation"); } + + SnapshotDataStoreVO snapshotStore = _snapshotDataStoreDao.findBySnapshot(snapshotId, DataStoreRole.Primary); + if (snapshotStore != null) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(snapshotStore.getDataStoreId()); + if (storagePoolVO.getPoolType() == Storage.StoragePoolType.PowerFlex) { + throw new InvalidParameterValueException("Create volume from snapshot is not supported for PowerFlex volume snapshots"); + } + } + parentVolume = _volsDao.findByIdIncludingRemoved(snapshotCheck.getVolumeId()); if (zoneId == null) { @@ -2183,6 +2196,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (_serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString()) != null) { throw new InvalidParameterValueException("Live Migration of GPU enabled VM is not supported"); } + + StoragePoolVO storagePoolVO = _storagePoolDao.findById(vol.getPoolId()); + if (storagePoolVO.getPoolType() == Storage.StoragePoolType.PowerFlex) { + throw new InvalidParameterValueException("Migrate volume of a running VM is unsupported on storage pool type " + storagePoolVO.getPoolType()); + } + // Check if the underlying hypervisor supports storage motion. Long hostId = vm.getHostId(); if (hostId != null) { @@ -2215,6 +2234,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new InvalidParameterValueException("Cannot migrate volume " + vol + "to the destination storage pool " + destPool.getName() + " as the storage pool is in maintenance mode."); } + if (!storageMgr.storagePoolCompatibleWithVolumePool(destPool, (Volume) vol)) { + throw new CloudRuntimeException("Storage pool " + destPool.getName() + " is not suitable to migrate volume " + vol.getName()); + } + if (!storageMgr.storagePoolHasEnoughSpace(Collections.singletonList(vol), destPool)) { throw new CloudRuntimeException("Storage pool " + destPool.getName() + " does not have enough space to migrate volume " + vol.getName()); } diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java index c900b2d14ba..b22d3b4ca3c 100644 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java @@ -77,6 +77,8 @@ public interface SnapshotManager extends Configurable { boolean canOperateOnVolume(Volume volume); + boolean backedUpSnapshotsExistsForVolume(Volume volume); + void cleanupSnapshotsByVolume(Long volumeId); Answer sendToPool(Volume vol, Command cmd); diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index a02c83be2c2..e4c5013c232 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1388,6 +1388,15 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement return true; } + @Override + public boolean backedUpSnapshotsExistsForVolume(Volume volume) { + List snapshots = _snapshotDao.listByStatus(volume.getId(), Snapshot.State.BackedUp); + if (snapshots.size() > 0) { + return true; + } + return false; + } + @Override public void cleanupSnapshotsByVolume(Long volumeId) { List infos = snapshotFactory.getSnapshots(volumeId, DataStoreRole.Primary); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 106709f34f8..582e8170d5a 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -789,7 +789,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return true; } - if (rebootVirtualMachine(userId, vmId) == null) { + if (rebootVirtualMachine(userId, vmId, false) == null) { s_logger.warn("Failed to reboot the vm " + vmInstance); return false; } else { @@ -900,7 +900,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir s_logger.debug("Vm " + vmInstance + " is stopped, not rebooting it as a part of SSH Key reset"); return true; } - if (rebootVirtualMachine(userId, vmId) == null) { + if (rebootVirtualMachine(userId, vmId, false) == null) { s_logger.warn("Failed to reboot the vm " + vmInstance); return false; } else { @@ -937,7 +937,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return status; } - private UserVm rebootVirtualMachine(long userId, long vmId) throws InsufficientCapacityException, ResourceUnavailableException { + private UserVm rebootVirtualMachine(long userId, long vmId, boolean forced) throws InsufficientCapacityException, ResourceUnavailableException { UserVmVO vm = _vmDao.findById(vmId); if (vm == null || vm.getState() == State.Destroyed || vm.getState() == State.Expunging || vm.getRemoved() != null) { @@ -948,6 +948,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (vm.getState() == State.Running && vm.getHostId() != null) { collectVmDiskStatistics(vm); collectVmNetworkStatistics(vm); + + if (forced) { + Host vmOnHost = _hostDao.findById(vm.getHostId()); + if (vmOnHost == null || vmOnHost.getResourceState() != ResourceState.Enabled || vmOnHost.getStatus() != Status.Up ) { + throw new CloudRuntimeException("Unable to force reboot the VM as the host: " + vm.getHostId() + " is not in the right state"); + } + return forceRebootVirtualMachine(vmId, vm.getHostId()); + } + DataCenterVO dc = _dcDao.findById(vm.getDataCenterId()); try { if (dc.getNetworkType() == DataCenter.NetworkType.Advanced) { @@ -971,7 +980,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new CloudRuntimeException("Concurrent operations on starting router. " + e); } catch (Exception ex){ throw new CloudRuntimeException("Router start failed due to" + ex); - }finally { + } finally { s_logger.info("Rebooting vm " + vm.getInstanceName()); _itMgr.reboot(vm.getUuid(), null); } @@ -982,6 +991,19 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + private UserVm forceRebootVirtualMachine(long vmId, long hostId) { + try { + if (stopVirtualMachine(vmId, false) != null) { + return startVirtualMachine(vmId, null, null, hostId, null, null).first(); + } + } catch (ResourceUnavailableException e) { + throw new CloudRuntimeException("Unable to reboot the VM: " + vmId, e); + } catch (CloudException e) { + throw new CloudRuntimeException("Unable to reboot the VM: " + vmId, e); + } + return null; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VM_UPGRADE, eventDescription = "upgrading Vm") /* @@ -2856,7 +2878,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir // Verify input parameters UserVmVO vmInstance = _vmDao.findById(vmId); if (vmInstance == null) { - throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); + throw new InvalidParameterValueException("Unable to find a virtual machine with id " + vmId); } _accountMgr.checkAccess(caller, null, true, vmInstance); @@ -2874,7 +2896,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new InvalidParameterValueException("Unable to find service offering: " + serviceOfferingId + " corresponding to the vm"); } - UserVm userVm = rebootVirtualMachine(CallContext.current().getCallingUserId(), vmId); + UserVm userVm = rebootVirtualMachine(CallContext.current().getCallingUserId(), vmId, cmd.isForced()); if (userVm != null ) { // update the vmIdCountMap if the vm is in advanced shared network with out services final List nics = _nicDao.listByVmId(vmId); diff --git a/server/src/test/java/com/cloud/vpc/MockVpcVirtualNetworkApplianceManager.java b/server/src/test/java/com/cloud/vpc/MockVpcVirtualNetworkApplianceManager.java index abb1863a1a3..e734fdfc8ad 100644 --- a/server/src/test/java/com/cloud/vpc/MockVpcVirtualNetworkApplianceManager.java +++ b/server/src/test/java/com/cloud/vpc/MockVpcVirtualNetworkApplianceManager.java @@ -110,7 +110,7 @@ public class MockVpcVirtualNetworkApplianceManager extends ManagerBase implement * @see com.cloud.network.VirtualNetworkApplianceService#rebootRouter(long, boolean) */ @Override - public VirtualRouter rebootRouter(final long routerId, final boolean reprogramNetwork) throws ConcurrentOperationException, ResourceUnavailableException { + public VirtualRouter rebootRouter(final long routerId, final boolean reprogramNetwork, final boolean forced) throws ConcurrentOperationException, ResourceUnavailableException { // TODO Auto-generated method stub return null; } diff --git a/test/integration/plugins/scaleio/README.md b/test/integration/plugins/scaleio/README.md new file mode 100644 index 00000000000..0e48fc15b46 --- /dev/null +++ b/test/integration/plugins/scaleio/README.md @@ -0,0 +1,46 @@ +# PowerFlex/ScaleIO storage plugin +================================== +This directory contains the basic VM, Volume life cycle tests for PowerFlex/ScaleIO storage pool (in KVM hypervisor). + +# Running tests +=============== +To run the basic volume tests, first update the below test data of the CloudStack environment + +```` +TestData.zoneId: +TestData.clusterId: +TestData.domainId: +TestData.url: +TestData.primaryStorage "url": +```` + +and to enable and run volume migration tests, update the below test data + +```` +TestData.migrationTests: True +TestData.primaryStorageSameInstance "url": +TestData.primaryStorageDistinctInstance "url": +```` + +PowerFlex/ScaleIO storage pool url format: + +```` +powerflex://:@/ + + where, + - : user name for API access + - : url-encoded password for API access + - : scaleio gateway host + - : storage pool name (case sensitive) + + +For example: "powerflex://admin:P%40ssword123@10.10.2.130/cspool" +```` + +Then run the tests using python unittest runner: nosetests + +```` +nosetests --with-marvin --marvin-config= /test/integration/plugins/scaleio/test_scaleio_volumes.py --zone= --hypervisor=kvm +```` + +You can also run these tests out of the box with PyDev or PyCharm or whatever. diff --git a/test/integration/plugins/scaleio/test_scaleio_volumes.py b/test/integration/plugins/scaleio/test_scaleio_volumes.py new file mode 100644 index 00000000000..c67f838297b --- /dev/null +++ b/test/integration/plugins/scaleio/test_scaleio_volumes.py @@ -0,0 +1,1213 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +import random +import time + +# All tests inherit from cloudstackTestCase +from marvin.cloudstackTestCase import cloudstackTestCase + +# Import Integration Libraries +# base - contains all resources as entities and defines create, delete, list operations on them +from marvin.lib.base import Account, DiskOffering, ServiceOffering, Snapshot, StoragePool, Template, User, VirtualMachine, Volume + +# common - commonly used methods for all tests are listed here +from marvin.lib.common import get_domain, get_template, get_zone, list_clusters, list_hosts, list_virtual_machines, \ + list_volumes + +# utils - utility classes for common cleanup, external library wrappers, etc. +from marvin.lib.utils import cleanup_resources, validateList +from marvin.codes import PASS +from nose.plugins.attrib import attr + +# Prerequisites: +# Only one zone +# Only one pod +# Only one cluster +# +# One PowerFlex/ScaleIO storage pool for basic tests +# Only KVM hypervisor is supported for PowerFlex/ScaleIO storage pool +# KVM host(s) with ScaleIO Data Client (SDC) installed and connected to Metadata Manager (MDM) +# +# For volume migration tests, additional storage pool(s) are required +# One PowerFlex/ScaleIO storage pool on the same ScaleIO storage cluster/instance +# One PowerFlex/ScaleIO storage pool on different ScaleIO storage cluster/instance +# + +class TestData(): + # constants + account = "account" + clusterId = "clusterId" + computeOffering = "computeoffering" + diskName = "diskname" + diskOffering = "diskoffering" + diskOfferingSameInstance = "diskOfferingSameInstance" + diskOfferingDistinctInstance = "diskOfferingDistinctInstance" + domainId = "domainId" + hypervisor = "hypervisor" + kvm = "kvm" + login = "login" + gatewayip = "gatewayip" + one_GB_in_bytes = 1073741824 + password = "password" + port = "port" + primaryStorage = "primarystorage" + primaryStorageSameInstance = "primaryStorageSameInstance" + primaryStorageDistinctInstance = "primaryStorageDistinctInstance" + provider = "provider" + scope = "scope" + powerFlex = "powerflex" + storageTag = "pflex" + storageTagSameInstance = "pflexsame" + storageTagDistinctInstance = "pflexdiff" + tags = "tags" + templateCacheNameKvm = "centos55-x86-64" + testAccount = "testaccount" + url = "url" + user = "user" + username = "username" + virtualMachine = "virtualmachine" + virtualMachine2 = "virtualmachine2" + virtualMachine3 = "virtualmachine3" + virtualMachine4 = "virtualmachine4" + volume_1 = "volume_1" + volume_2 = "volume_2" + volume_3 = "volume_3" + volume_4 = "volume_4" + kvm = "kvm" + zoneId = "zoneId" + migrationTests = "migrationTests" + + # hypervisor type to test + hypervisor_type = kvm + + def __init__(self): + self.testdata = { + TestData.kvm: { + TestData.username: "root", + TestData.password: "P@ssword123" + }, + TestData.account: { + "email": "test1@test1.com", + "firstname": "John", + "lastname": "Doe", + "username": "test1", + "password": "test" + }, + TestData.testAccount: { + "email": "test2@test2.com", + "firstname": "Jane", + "lastname": "Doe", + "username": "test2", + "password": "test" + }, + TestData.user: { + "email": "user@test1.com", + "firstname": "Jane", + "lastname": "Doe", + "username": "test1user", + "password": "password" + }, + TestData.primaryStorage: { + "name": "PowerFlexPool-%d" % random.randint(0, 100), + TestData.scope: "ZONE", + "url": "powerflex://admin:P%40ssword123@10.10.4.141/cspool01", + TestData.provider: "PowerFlex", + TestData.tags: TestData.storageTag + "," + TestData.storageTagSameInstance + "," + TestData.storageTagDistinctInstance, + TestData.hypervisor: "KVM" + }, + TestData.virtualMachine: { + "name": "TestVM1", + "displayname": "Test VM 1" + }, + TestData.virtualMachine2: { + "name": "TestVM2", + "displayname": "Test VM 2" + }, + TestData.virtualMachine3: { + "name": "TestVM3", + "displayname": "Test VM 3" + }, + TestData.virtualMachine4: { + "name": "TestVM4", + "displayname": "Test VM 4" + }, + TestData.computeOffering: { + "name": "PowerFlex_Compute", + "displaytext": "PowerFlex_Compute", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + TestData.tags: TestData.storageTag + }, + TestData.diskOffering: { + "name": "PowerFlex_Disk", + "displaytext": "PowerFlex_Disk", + "disksize": 8, + TestData.tags: TestData.storageTag, + "storagetype": "shared" + }, + TestData.volume_1: { + TestData.diskName: "test-volume-1", + }, + TestData.volume_2: { + TestData.diskName: "test-volume-2", + }, + TestData.volume_3: { + TestData.diskName: "test-volume-3", + }, + TestData.volume_4: { + TestData.diskName: "test-volume-4", + }, + TestData.zoneId: 1, + TestData.clusterId: 1, + TestData.domainId: 1, + TestData.url: "10.10.3.226", + # for volume migration tests + TestData.migrationTests: True, + # PowerFlex/ScaleIO storage pool on the same ScaleIO storage instance + TestData.primaryStorageSameInstance: { + "name": "PowerFlexPool-%d" % random.randint(0, 100), + TestData.scope: "ZONE", + "url": "powerflex://admin:P%40ssword123@10.10.4.141/cspool02", + TestData.provider: "PowerFlex", + TestData.tags: TestData.storageTag + "," + TestData.storageTagSameInstance, + TestData.hypervisor: "KVM" + }, + # PowerFlex/ScaleIO storage pool on different ScaleIO storage instance + TestData.primaryStorageDistinctInstance: { + "name": "PowerFlexPool-%d" % random.randint(0, 100), + TestData.scope: "ZONE", + "url": "powerflex://admin:P%40ssword123@10.10.4.194/cloudstackpool", + TestData.provider: "PowerFlex", + TestData.tags: TestData.storageTag + "," + TestData.storageTagDistinctInstance, + TestData.hypervisor: "KVM" + }, + TestData.diskOfferingSameInstance: { + "name": "PowerFlex_Disk_Same_Inst", + "displaytext": "PowerFlex_Disk_Same_Inst", + "disksize": 8, + TestData.tags: TestData.storageTagSameInstance, + "storagetype": "shared" + }, + TestData.diskOfferingDistinctInstance: { + "name": "PowerFlex_Disk_Diff_Inst", + "displaytext": "PowerFlex_Disk_Diff_Inst", + "disksize": 8, + TestData.tags: TestData.storageTagDistinctInstance, + "storagetype": "shared" + }, + } + + +class TestScaleIOVolumes(cloudstackTestCase): + _volume_vm_id_and_vm_id_do_not_match_err_msg = "The volume's VM ID and the VM's ID do not match." + _vm_not_in_running_state_err_msg = "The VM is not in the 'Running' state." + _vm_not_in_stopped_state_err_msg = "The VM is not in the 'Stopped' state." + + @classmethod + def setUpClass(cls): + # Set up API client + testclient = super(TestScaleIOVolumes, cls).getClsTestClient() + + cls.apiClient = testclient.getApiClient() + cls.configData = testclient.getParsedTestDataConfig() + cls.dbConnection = testclient.getDbConnection() + cls.testdata = TestData().testdata + + # Get Resources from Cloud Infrastructure + cls.zone = get_zone(cls.apiClient, zone_id=cls.testdata[TestData.zoneId]) + cls.cluster = list_clusters(cls.apiClient)[0] + cls.template = get_template(cls.apiClient, cls.zone.id, hypervisor=TestData.hypervisor_type) + cls.domain = get_domain(cls.apiClient, cls.testdata[TestData.domainId]) + + # Create test account + cls.account = Account.create( + cls.apiClient, + cls.testdata["account"], + admin=1 + ) + + # Set up connection to make customized API calls + cls.user = User.create( + cls.apiClient, + cls.testdata["user"], + account=cls.account.name, + domainid=cls.domain.id + ) + + url = cls.testdata[TestData.url] + + primarystorage = cls.testdata[TestData.primaryStorage] + + cls.primary_storage = StoragePool.create( + cls.apiClient, + primarystorage, + scope=primarystorage[TestData.scope], + zoneid=cls.zone.id, + provider=primarystorage[TestData.provider], + tags=primarystorage[TestData.tags], + hypervisor=primarystorage[TestData.hypervisor] + ) + + cls.compute_offering = ServiceOffering.create( + cls.apiClient, + cls.testdata[TestData.computeOffering] + ) + + cls.disk_offering = DiskOffering.create( + cls.apiClient, + cls.testdata[TestData.diskOffering] + ) + + if TestData.migrationTests: + primarystorage_sameinst = cls.testdata[TestData.primaryStorageSameInstance] + cls.primary_storage_same_inst = StoragePool.create( + cls.apiClient, + primarystorage_sameinst, + scope=primarystorage_sameinst[TestData.scope], + zoneid=cls.zone.id, + provider=primarystorage_sameinst[TestData.provider], + tags=primarystorage_sameinst[TestData.tags], + hypervisor=primarystorage_sameinst[TestData.hypervisor] + ) + + primarystorage_distinctinst = cls.testdata[TestData.primaryStorageDistinctInstance] + cls.primary_storage_distinct_inst = StoragePool.create( + cls.apiClient, + primarystorage_distinctinst, + scope=primarystorage_distinctinst[TestData.scope], + zoneid=cls.zone.id, + provider=primarystorage_distinctinst[TestData.provider], + tags=primarystorage_distinctinst[TestData.tags], + hypervisor=primarystorage_distinctinst[TestData.hypervisor] + ) + + cls.disk_offering_same_inst = DiskOffering.create( + cls.apiClient, + cls.testdata[TestData.diskOfferingSameInstance] + ) + + cls.disk_offering_distinct_inst = DiskOffering.create( + cls.apiClient, + cls.testdata[TestData.diskOfferingDistinctInstance] + ) + + + # Create VM and volume for tests + cls.virtual_machine = VirtualMachine.create( + cls.apiClient, + cls.testdata[TestData.virtualMachine], + accountid=cls.account.name, + zoneid=cls.zone.id, + serviceofferingid=cls.compute_offering.id, + templateid=cls.template.id, + domainid=cls.domain.id, + startvm=False + ) + + TestScaleIOVolumes._start_vm(cls.virtual_machine) + + cls.volume = Volume.create( + cls.apiClient, + cls.testdata[TestData.volume_1], + account=cls.account.name, + domainid=cls.domain.id, + zoneid=cls.zone.id, + diskofferingid=cls.disk_offering.id + ) + + # Resources that are to be destroyed + cls._cleanup = [ + cls.volume, + cls.virtual_machine, + cls.compute_offering, + cls.disk_offering, + cls.user, + cls.account + ] + + @classmethod + def tearDownClass(cls): + try: + if TestData.migrationTests: + cls._cleanup.append(cls.disk_offering_same_inst) + cls._cleanup.append(cls.disk_offering_distinct_inst) + + cleanup_resources(cls.apiClient, cls._cleanup) + + cls.primary_storage.delete(cls.apiClient) + + if TestData.migrationTests: + cls.primary_storage_same_inst.delete(cls.apiClient) + cls.primary_storage_distinct_inst.delete(cls.apiClient) + + except Exception as e: + logging.debug("Exception in tearDownClass(cls): %s" % e) + + def setUp(self): + self.attached = False + self.cleanup = [] + + def tearDown(self): + if self.attached: + self.virtual_machine.detach_volume(self.apiClient, self.volume) + + cleanup_resources(self.apiClient, self.cleanup) + + @attr(tags=['basic'], required_hardware=False) + def test_01_create_vm_with_volume(self): + '''Create VM with attached volume and expunge VM''' + + ####################################### + # STEP 1: Create VM and attach volume # + ####################################### + + test_virtual_machine = VirtualMachine.create( + self.apiClient, + self.testdata[TestData.virtualMachine2], + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering.id, + templateid=self.template.id, + domainid=self.domain.id, + startvm=False + ) + + TestScaleIOVolumes._start_vm(test_virtual_machine) + + self.volume = test_virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(test_virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ####################################### + # STEP 2: Destroy and Expunge VM # + ####################################### + + test_virtual_machine.delete(self.apiClient, True) + + self.attached = False + + vol = self._get_volume(self.volume.id) + + self.assertEqual( + vol.virtualmachineid, + None, + "Check if attached to virtual machine" + ) + + self.assertEqual( + vol.vmname, + None, + "Check if VM was expunged" + ) + + list_virtual_machine_response = list_virtual_machines( + self.apiClient, + id=test_virtual_machine.id + ) + + self.assertEqual( + list_virtual_machine_response, + None, + "Check if VM was actually expunged" + ) + + @attr(tags=['basic'], required_hardware=False) + def test_02_attach_new_volume_to_stopped_vm(self): + '''Attach a volume to a stopped virtual machine, then start VM''' + + self.virtual_machine.stop(self.apiClient) + + new_volume = Volume.create( + self.apiClient, + self.testdata[TestData.volume_2], + account=self.account.name, + domainid=self.domain.id, + zoneid=self.zone.id, + diskofferingid=self.disk_offering.id + ) + + self.cleanup.append(new_volume) + + new_volume = self.virtual_machine.attach_volume( + self.apiClient, + new_volume + ) + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + "running", + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + # Detach volume + new_volume = self.virtual_machine.detach_volume( + self.apiClient, + new_volume + ) + + self.assertEqual( + new_volume.virtualmachineid, + None, + "The volume should not be attached to a VM." + ) + + @attr(tags=['basic'], required_hardware=False) + def test_03_attach_detach_attach_volume_to_vm(self): + '''Attach, detach, and attach volume to a running VM''' + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + ####################################### + # STEP 1: Attach volume to running VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ######################################### + # STEP 2: Detach volume from running VM # + ######################################### + + self.volume = self.virtual_machine.detach_volume( + self.apiClient, + self.volume + ) + + self.attached = False + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + None, + "The volume should not be attached to a VM." + ) + + self.assertEqual( + vm.state.lower(), + 'running', + str(vm.state) + ) + + ####################################### + # STEP 3: Attach volume to running VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + @attr(tags=['basic'], required_hardware=False) + def test_04_detach_vol_stopped_vm_start(self): + '''Detach volume from a stopped VM, then start.''' + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + ####################################### + # STEP 1: Attach volume to running VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ######################################### + # STEP 2: Detach volume from stopped VM # + ######################################### + + self.virtual_machine.stop(self.apiClient) + + self.volume = self.virtual_machine.detach_volume( + self.apiClient, + self.volume + ) + + self.attached = False + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + None, + "The volume should not be attached to a VM." + ) + + self.assertEqual( + vm.state.lower(), + 'stopped', + TestScaleIOVolumes._vm_not_in_stopped_state_err_msg + ) + + ####################################### + # STEP 3: Start VM with detached vol # + ####################################### + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + @attr(tags=['basic'], required_hardware=False) + def test_05_attach_volume_to_stopped_vm(self): + '''Attach a volume to a stopped virtual machine, then start VM''' + + self.virtual_machine.stop(self.apiClient) + + ####################################### + # STEP 1: Attach volume to stopped VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'stopped', + TestScaleIOVolumes._vm_not_in_stopped_state_err_msg + ) + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + @attr(tags=['basic'], required_hardware=False) + def test_06_attached_volume_reboot_vm(self): + '''Attach volume to running VM, then reboot.''' + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + ####################################### + # STEP 1: Attach volume to running VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ####################################### + # STEP 2: Reboot VM with attached vol # + ####################################### + TestScaleIOVolumes._reboot_vm(self.virtual_machine) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + @attr(tags=['basic'], required_hardware=False) + def test_07_detach_volume_reboot_vm(self): + '''Detach volume from a running VM, then reboot.''' + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + ####################################### + # STEP 1: Attach volume to running VM # + ####################################### + + self.volume = self.virtual_machine.attach_volume( + self.apiClient, + self.volume + ) + + self.attached = True + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + vm.id, + TestScaleIOVolumes._volume_vm_id_and_vm_id_do_not_match_err_msg + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ######################################### + # STEP 2: Detach volume from running VM # + ######################################### + + self.volume = self.virtual_machine.detach_volume( + self.apiClient, + self.volume + ) + + self.attached = False + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + self.volume.virtualmachineid, + None, + "The volume should not be attached to a VM." + ) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + ####################################### + # STEP 3: Reboot VM with detached vol # + ####################################### + + self.virtual_machine.reboot(self.apiClient) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + 'running', + TestScaleIOVolumes._vm_not_in_running_state_err_msg + ) + + @attr(tags=['basic'], required_hardware=False) + def test_08_delete_volume_was_attached(self): + '''Delete volume that was attached to a VM and is detached now''' + + TestScaleIOVolumes._start_vm(self.virtual_machine) + + ####################################### + # STEP 1: Create vol and attach to VM # + ####################################### + + new_volume = Volume.create( + self.apiClient, + self.testdata[TestData.volume_2], + account=self.account.name, + domainid=self.domain.id, + zoneid=self.zone.id, + diskofferingid=self.disk_offering.id + ) + + volume_to_delete_later = new_volume + + new_volume = self.virtual_machine.attach_volume( + self.apiClient, + new_volume + ) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + new_volume.virtualmachineid, + vm.id, + "Check if attached to virtual machine" + ) + + self.assertEqual( + vm.state.lower(), + 'running', + str(vm.state) + ) + + ####################################### + # STEP 2: Detach and delete volume # + ####################################### + + new_volume = self.virtual_machine.detach_volume( + self.apiClient, + new_volume + ) + + vm = self._get_vm(self.virtual_machine.id) + + self.assertEqual( + new_volume.virtualmachineid, + None, + "Check if attached to virtual machine" + ) + + self.assertEqual( + vm.state.lower(), + 'running', + str(vm.state) + ) + + volume_to_delete_later.delete(self.apiClient) + + list_volumes_response = list_volumes( + self.apiClient, + id=new_volume.id + ) + + self.assertEqual( + list_volumes_response, + None, + "Check volume was deleted" + ) + + @attr(tags=['advanced', 'migration'], required_hardware=False) + def test_09_migrate_volume_to_same_instance_pool(self): + '''Migrate volume to the same instance pool''' + + if not TestData.migrationTests: + self.skipTest("Volume migration tests not enabled, skipping test") + + ####################################### + # STEP 1: Create VM and Start VM # + ####################################### + + test_virtual_machine = VirtualMachine.create( + self.apiClient, + self.testdata[TestData.virtualMachine3], + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering.id, + templateid=self.template.id, + domainid=self.domain.id, + startvm=False + ) + + TestScaleIOVolumes._start_vm(test_virtual_machine) + + ####################################### + # STEP 2: Create vol and attach to VM # + ####################################### + + new_volume = Volume.create( + self.apiClient, + self.testdata[TestData.volume_3], + account=self.account.name, + domainid=self.domain.id, + zoneid=self.zone.id, + diskofferingid=self.disk_offering_same_inst.id + ) + + volume_to_delete_later = new_volume + + new_volume = test_virtual_machine.attach_volume( + self.apiClient, + new_volume + ) + + vm = self._get_vm(test_virtual_machine.id) + + self.assertEqual( + new_volume.virtualmachineid, + vm.id, + "Check if attached to virtual machine" + ) + + self.assertEqual( + vm.state.lower(), + 'running', + str(vm.state) + ) + + ####################################### + # STEP 3: Stop VM and Migrate volume # + ####################################### + + test_virtual_machine.stop(self.apiClient) + + vm = self._get_vm(test_virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + 'stopped', + str(vm.state) + ) + + pools = StoragePool.listForMigration( + self.apiClient, + id=new_volume.id + ) + + if not pools: + self.skipTest("No suitable storage pools found for volume migration, skipping test") + + self.assertEqual( + validateList(pools)[0], + PASS, + "Invalid pool response from findStoragePoolsForMigration API" + ) + + pool = pools[0] + self.debug("Migrating Volume-ID: %s to Same Instance Pool: %s" % (new_volume.id, pool.id)) + + try: + Volume.migrate( + self.apiClient, + volumeid=new_volume.id, + storageid=pool.id + ) + except Exception as e: + self.fail("Volume migration failed with error %s" % e) + + ####################################### + # STEP 4: Detach and delete volume # + ####################################### + + new_volume = test_virtual_machine.detach_volume( + self.apiClient, + new_volume + ) + + self.assertEqual( + new_volume.virtualmachineid, + None, + "Check if attached to virtual machine" + ) + + volume_to_delete_later.delete(self.apiClient) + + list_volumes_response = list_volumes( + self.apiClient, + id=new_volume.id + ) + + self.assertEqual( + list_volumes_response, + None, + "Check volume was deleted" + ) + + ####################################### + # STEP 4: Delete VM # + ####################################### + + test_virtual_machine.delete(self.apiClient, True) + + @attr(tags=['advanced', 'migration'], required_hardware=False) + def test_10_migrate_volume_to_distinct_instance_pool(self): + '''Migrate volume to distinct instance pool''' + + if not TestData.migrationTests: + self.skipTest("Volume migration tests not enabled, skipping test") + + ####################################### + # STEP 1: Create VM and Start VM # + ####################################### + + test_virtual_machine = VirtualMachine.create( + self.apiClient, + self.testdata[TestData.virtualMachine4], + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering.id, + templateid=self.template.id, + domainid=self.domain.id, + startvm=False + ) + + TestScaleIOVolumes._start_vm(test_virtual_machine) + + ####################################### + # STEP 2: Create vol and attach to VM # + ####################################### + + new_volume = Volume.create( + self.apiClient, + self.testdata[TestData.volume_4], + account=self.account.name, + domainid=self.domain.id, + zoneid=self.zone.id, + diskofferingid=self.disk_offering_distinct_inst.id + ) + + volume_to_delete_later = new_volume + + new_volume = test_virtual_machine.attach_volume( + self.apiClient, + new_volume + ) + + vm = self._get_vm(test_virtual_machine.id) + + self.assertEqual( + new_volume.virtualmachineid, + vm.id, + "Check if attached to virtual machine" + ) + + self.assertEqual( + vm.state.lower(), + 'running', + str(vm.state) + ) + + ####################################### + # STEP 3: Stop VM and Migrate volume # + ####################################### + + test_virtual_machine.stop(self.apiClient) + + vm = self._get_vm(test_virtual_machine.id) + + self.assertEqual( + vm.state.lower(), + 'stopped', + str(vm.state) + ) + + pools = StoragePool.listForMigration( + self.apiClient, + id=new_volume.id + ) + + if not pools: + self.skipTest("No suitable storage pools found for volume migration, skipping test") + + self.assertEqual( + validateList(pools)[0], + PASS, + "Invalid pool response from findStoragePoolsForMigration API" + ) + + pool = pools[0] + self.debug("Migrating Volume-ID: %s to Distinct Instance Pool: %s" % (new_volume.id, pool.id)) + + try: + Volume.migrate( + self.apiClient, + volumeid=new_volume.id, + storageid=pool.id + ) + except Exception as e: + self.fail("Volume migration failed with error %s" % e) + + ####################################### + # STEP 4: Detach and delete volume # + ####################################### + + new_volume = test_virtual_machine.detach_volume( + self.apiClient, + new_volume + ) + + self.assertEqual( + new_volume.virtualmachineid, + None, + "Check if attached to virtual machine" + ) + + volume_to_delete_later.delete(self.apiClient) + + list_volumes_response = list_volumes( + self.apiClient, + id=new_volume.id + ) + + self.assertEqual( + list_volumes_response, + None, + "Check volume was deleted" + ) + + ####################################### + # STEP 4: Delete VM # + ####################################### + + test_virtual_machine.delete(self.apiClient, True) + + + def _create_vm_using_template_and_destroy_vm(self, template): + vm_name = "VM-%d" % random.randint(0, 100) + + virtual_machine_dict = {"name": vm_name, "displayname": vm_name} + + virtual_machine = VirtualMachine.create( + self.apiClient, + virtual_machine_dict, + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering.id, + templateid=template.id, + domainid=self.domain.id, + startvm=True + ) + + list_volumes_response = list_volumes( + self.apiClient, + virtualmachineid=virtual_machine.id, + listall=True + ) + + vm_root_volume = list_volumes_response[0] + vm_root_volume_name = vm_root_volume.name + + virtual_machine.delete(self.apiClient, True) + + def _get_bytes_from_gb(self, number_in_gb): + return number_in_gb * 1024 * 1024 * 1024 + + def _get_volume(self, volume_id): + list_vols_response = list_volumes(self.apiClient, id=volume_id) + return list_vols_response[0] + + def _get_vm(self, vm_id): + list_vms_response = list_virtual_machines(self.apiClient, id=vm_id) + return list_vms_response[0] + + def _get_template_cache_name(self): + if TestData.hypervisor_type == TestData.kvm: + return TestData.templateCacheNameKvm + + self.assert_(False, "Invalid hypervisor type") + + + @classmethod + def _start_vm(cls, vm): + vm_for_check = list_virtual_machines( + cls.apiClient, + id=vm.id + )[0] + + if vm_for_check.state == VirtualMachine.STOPPED: + vm.start(cls.apiClient) + + # For KVM, just give it 90 seconds to boot up. + if TestData.hypervisor_type == TestData.kvm: + time.sleep(90) + + @classmethod + def _reboot_vm(cls, vm): + vm.reboot(cls.apiClient) + + # For KVM, just give it 90 seconds to boot up. + if TestData.hypervisor_type == TestData.kvm: + time.sleep(90) diff --git a/test/integration/smoke/test_routers.py b/test/integration/smoke/test_routers.py index f84afdb4529..dd13b7f8194 100644 --- a/test/integration/smoke/test_routers.py +++ b/test/integration/smoke/test_routers.py @@ -821,3 +821,46 @@ class TestRouterServices(cloudstackTestCase): "Router response after reboot is either is invalid\ or in stopped state") return + + @attr(tags=["advanced", "advancedns", "smoke", "dvs"], required_hardware="false") + def test_10_reboot_router_forced(self): + """Test force reboot router + """ + + list_router_response = list_routers( + self.apiclient, + account=self.account.name, + domainid=self.account.domainid + ) + self.assertEqual( + isinstance(list_router_response, list), + True, + "Check list response returns a valid list" + ) + router = list_router_response[0] + + public_ip = router.publicip + + self.debug("Force rebooting the router with ID: %s" % router.id) + # Reboot the router + cmd = rebootRouter.rebootRouterCmd() + cmd.id = router.id + cmd.forced = True + self.apiclient.rebootRouter(cmd) + + # List routers to check state of router + retries_cnt = 10 + while retries_cnt >= 0: + router_response = list_routers( + self.apiclient, + id=router.id + ) + if self.verifyRouterResponse(router_response, public_ip): + self.debug("Router is running successfully after force reboot") + return + time.sleep(10) + retries_cnt = retries_cnt - 1 + self.fail( + "Router response after force reboot is either invalid\ + or router in stopped state") + return diff --git a/test/integration/smoke/test_ssvm.py b/test/integration/smoke/test_ssvm.py index ad5c4ab344b..fb7e1b1ee3d 100644 --- a/test/integration/smoke/test_ssvm.py +++ b/test/integration/smoke/test_ssvm.py @@ -957,7 +957,122 @@ class TestSSVMs(cloudstackTestCase): "basic", "sg"], required_hardware="true") - def test_09_destroy_ssvm(self): + def test_09_reboot_ssvm_forced(self): + """Test force reboot SSVM + """ + + list_ssvm_response = list_ssvms( + self.apiclient, + systemvmtype='secondarystoragevm', + state='Running', + zoneid=self.zone.id + ) + + self.assertEqual( + isinstance(list_ssvm_response, list), + True, + "Check list response returns a valid list" + ) + + ssvm_response = list_ssvm_response[0] + + hosts = list_hosts( + self.apiclient, + id=ssvm_response.hostid + ) + self.assertEqual( + isinstance(hosts, list), + True, + "Check list response returns a valid list" + ) + + self.debug("Force rebooting SSVM: %s" % ssvm_response.id) + cmd = rebootSystemVm.rebootSystemVmCmd() + cmd.id = ssvm_response.id + cmd.forced = True + self.apiclient.rebootSystemVm(cmd) + + ssvm_response = self.checkForRunningSystemVM(ssvm_response) + self.debug("SSVM State: %s" % ssvm_response.state) + self.assertEqual( + 'Running', + str(ssvm_response.state), + "Check whether SSVM is running or not" + ) + + # Wait for the agent to be up + self.waitForSystemVMAgent(ssvm_response.name) + + # Wait until NFS stores mounted before running the script + time.sleep(90) + # Call to verify cloud process is running + self.test_03_ssvm_internals() + + @attr( + tags=[ + "advanced", + "advancedns", + "smoke", + "basic", + "sg"], + required_hardware="true") + def test_10_reboot_cpvm_forced(self): + """Test force reboot CPVM + """ + + list_cpvm_response = list_ssvms( + self.apiclient, + systemvmtype='consoleproxy', + state='Running', + zoneid=self.zone.id + ) + self.assertEqual( + isinstance(list_cpvm_response, list), + True, + "Check list response returns a valid list" + ) + cpvm_response = list_cpvm_response[0] + + hosts = list_hosts( + self.apiclient, + id=cpvm_response.hostid + ) + self.assertEqual( + isinstance(hosts, list), + True, + "Check list response returns a valid list" + ) + + self.debug("Force rebooting CPVM: %s" % cpvm_response.id) + + cmd = rebootSystemVm.rebootSystemVmCmd() + cmd.id = cpvm_response.id + cmd.forced = True + self.apiclient.rebootSystemVm(cmd) + + cpvm_response = self.checkForRunningSystemVM(cpvm_response) + self.debug("CPVM state: %s" % cpvm_response.state) + self.assertEqual( + 'Running', + str(cpvm_response.state), + "Check whether CPVM is running or not" + ) + + # Wait for the agent to be up + self.waitForSystemVMAgent(cpvm_response.name) + + # Call to verify cloud process is running + self.test_04_cpvm_internals() + + @attr( + tags=[ + "advanced", + "advancedns", + "smoke", + "basic", + "sg"], + required_hardware="true") + def test_11_destroy_ssvm(self): """Test destroy SSVM """ @@ -1029,7 +1144,7 @@ class TestSSVMs(cloudstackTestCase): "basic", "sg"], required_hardware="true") - def test_10_destroy_cpvm(self): + def test_12_destroy_cpvm(self): """Test destroy CPVM """ @@ -1100,7 +1215,7 @@ class TestSSVMs(cloudstackTestCase): "basic", "sg"], required_hardware="true") - def test_11_ss_nfs_version_on_ssvm(self): + def test_13_ss_nfs_version_on_ssvm(self): """Test NFS Version on Secondary Storage mounted properly on SSVM """ diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index 3def05346a4..6360828a414 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -484,6 +484,40 @@ class TestVMLifeCycle(cloudstackTestCase): ) return + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_04_reboot_vm_forced(self): + """Test Force Reboot Virtual Machine + """ + + try: + self.debug("Force rebooting VM - ID: %s" % self.virtual_machine.id) + self.small_virtual_machine.reboot(self.apiclient, forced=True) + except Exception as e: + self.fail("Failed to force reboot VM: %s" % e) + + list_vm_response = VirtualMachine.list( + self.apiclient, + id=self.small_virtual_machine.id + ) + self.assertEqual( + isinstance(list_vm_response, list), + True, + "Check list response returns a valid list" + ) + + self.assertNotEqual( + len(list_vm_response), + 0, + "Check VM available in List Virtual Machines" + ) + + self.assertEqual( + list_vm_response[0].state, + "Running", + "Check virtual machine is in running state" + ) + return + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") def test_06_destroy_vm(self): """Test destroy Virtual Machine diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index df38bb54a2b..bb0047551bc 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -679,10 +679,12 @@ class VirtualMachine: raise Exception(response[1]) return - def reboot(self, apiclient): + def reboot(self, apiclient, forced=None): """Reboot the instance""" cmd = rebootVirtualMachine.rebootVirtualMachineCmd() cmd.id = self.id + if forced: + cmd.forced = forced apiclient.rebootVirtualMachine(cmd) response = self.getState(apiclient, VirtualMachine.RUNNING) @@ -4323,10 +4325,12 @@ class Router: return apiclient.stopRouter(cmd) @classmethod - def reboot(cls, apiclient, id): + def reboot(cls, apiclient, id, forced=None): """Reboots the router""" cmd = rebootRouter.rebootRouterCmd() cmd.id = id + if forced: + cmd.forced = forced return apiclient.rebootRouter(cmd) @classmethod diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index a2da335463d..dab420eb31f 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -4006,7 +4006,7 @@ table tr.selected td.actions .action.disabled .icon { .ui-dialog div.form-container div.name label { display: block; - width: 119px; + width: 127px; margin-top: 2px; font-size: 13px; text-align: right; diff --git a/ui/l10n/ar.js b/ui/l10n/ar.js index acfc5539c2a..c9bb9a5cd42 100644 --- a/ui/l10n/ar.js +++ b/ui/l10n/ar.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Your internet name cannot be resolved.", "force.delete": "Force Delete", "force.delete.domain.warning": "Warning: Choosing this option will cause the deletion of all child domains and all associated accounts and their resources.", + "force.reboot":"Force Reboot", "force.remove": "Force Remove", "force.remove.host.warning": "Warning: Choosing this option will cause CloudStack to forcefully stop all running virtual machines before removing this host from the cluster.", "force.stop": "Force Stop", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Power State", "label.prev": "Prev", "label.previous": "السابق", diff --git a/ui/l10n/ca.js b/ui/l10n/ca.js index 3a7c0459402..3fac2a09a90 100644 --- a/ui/l10n/ca.js +++ b/ui/l10n/ca.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Your internet name cannot be resolved.", "force.delete": "Force Delete", "force.delete.domain.warning": "Warning: Choosing this option will cause the deletion of all child domains and all associated accounts and their resources.", + "force.reboot":"Force Reboot", "force.remove": "Force Remove", "force.remove.host.warning": "Warning: Choosing this option will cause CloudStack to forcefully stop all running virtual machines before removing this host from the cluster.", "force.stop": "Force Stop", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Power State", "label.prev": "Prev", "label.previous": "Anterior", diff --git a/ui/l10n/de_DE.js b/ui/l10n/de_DE.js index 4fbc827c013..125a5495fd1 100644 --- a/ui/l10n/de_DE.js +++ b/ui/l10n/de_DE.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Ihr Internetname kann nicht aufgelöst werden.", "force.delete": "Erzwinge Löschung", "force.delete.domain.warning": "Achtung: Diese Auswahl führt zu einer Löschung aller untergeordneten Domains und aller angeschlossenen Konten sowie ihrer Quellen.", + "force.reboot":"Neustart erzwingen", "force.remove": "Erzwinge Entfernung", "force.remove.host.warning": "Achtung: Diese Auswahl wird CloudStack zum sofortigen Anhalten der virtuellen Maschine führen, bevor der Host vom Cluster entfernt wurde.", "force.stop": "Erzwinge Stopp", @@ -1263,6 +1264,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP-Bereichsdetails", "label.portable.ip.ranges": "Portable IP-Bereiche", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Betriebszustand", "label.prev": "Vor", "label.previous": "Vorherige", diff --git a/ui/l10n/en.js b/ui/l10n/en.js index cb967beca67..9abafb09922 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name":"Your internet name cannot be resolved.", "force.delete":"Force Delete", "force.delete.domain.warning":"Warning: Choosing this option will cause the deletion of all child domains and all associated accounts and their resources.", +"force.reboot":"Force Reboot", "force.remove":"Force Remove", "force.remove.host.warning":"Warning: Choosing this option will cause CloudStack to forcefully stop all running virtual machines before removing this host from the cluster.", "force.stop":"Force Stop", @@ -1361,6 +1362,10 @@ var dictionary = { "label.portable.ip.range.details":"Portable IP Range details", "label.portable.ip.ranges":"Portable IP Ranges", "label.portable.ips":"Portable IPs", +"label.powerflex.gateway": "Gateway", +"label.powerflex.gateway.username": "Gateway Username", +"label.powerflex.gateway.password": "Gateway Password", +"label.powerflex.storage.pool": "Storage Pool", "label.powerstate":"Power State", "label.prev":"Prev", "label.previous":"Previous", diff --git a/ui/l10n/es.js b/ui/l10n/es.js index 875b7a16d32..43430f4ac4d 100644 --- a/ui/l10n/es.js +++ b/ui/l10n/es.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "El nombre de Internet no se puede resolver.", "force.delete": "Forzar Borrado", "force.delete.domain.warning": "Advertencia: Elegir esta opción, provocará la eliminación de todos los dominios hijos y todas las cuentas asociadas y sus recursos.", + "force.reboot":"Forzar reinicio", "force.remove": "Forzar el retiro", "force.remove.host.warning": "Advertencia: Elegir esta opción provocará que CloudStack detenga a la fuerza todas las máquinas virtuales antes de eliminar este host del clúster.", "force.stop": "Forzar Parar", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Detalles del Rango de IP portátil", "label.portable.ip.ranges": "Rangos de IP portátiles", "label.portable.ips": "IPs Portátiles", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Estado de la Alimentación", "label.prev": "Anterior", "label.previous": "Previo", diff --git a/ui/l10n/fr_FR.js b/ui/l10n/fr_FR.js index 92eb8de3e8b..5bdc774e6dc 100644 --- a/ui/l10n/fr_FR.js +++ b/ui/l10n/fr_FR.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Votre nom Internet ne peut pas être résolu.", "force.delete": "Forcer la suppression", "force.delete.domain.warning": "Attention : Choisir cette option entraînera la suppression de tous les domaines issus et l'ensemble des comptes associés, ainsi que de leur ressources", + "force.reboot":"Forcer le redémarrage", "force.remove": "Suppression forcée", "force.remove.host.warning": "Attention : Choisir cette option entraînera CloudStack à forcer l'arrêt de l'ensemble des machines virtuelles avant d'enlever cet hôte du cluster", "force.stop": "Forcer l'arrêt", @@ -1263,6 +1264,10 @@ var dictionary = { "label.portable.ip.range.details": "Détails Plages IP portables", "label.portable.ip.ranges": "Plages IP portables", "label.portable.ips": "IPs portables", + "label.powerflex.gateway": "passerelle", + "label.powerflex.gateway.username": "Nom d'utilisateur de la passerelle", + "label.powerflex.gateway.password": "Mot de passe de la passerelle", + "label.powerflex.storage.pool": "Pool de stockage", "label.powerstate": "Status Alimentation", "label.prev": "Précédent", "label.previous": "Retour", diff --git a/ui/l10n/hu.js b/ui/l10n/hu.js index 6912c1ab624..b4749d4a846 100644 --- a/ui/l10n/hu.js +++ b/ui/l10n/hu.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Az internet neved nem oldható fel.", "force.delete": "Törlés kikényszerítése", "force.delete.domain.warning": "Figyelmeztetés: Ha ezt választod, törlődni fog minden alárendelt domén és minden kapcsolódó számla és a hozzájuk tartozó erőforrások.", + "force.reboot":"Kényszer újraindítás", "force.remove": "Eltávolítás kikényszerítése", "force.remove.host.warning": "Figyelmeztetés: Ha ezt az opciót választod, a CloudStack minden virtuális gépet leállít mielőtt eltávolítja a kiszolgálót a fürtből.", "force.stop": "Leállás kikényszerítése", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Hordozható IP tartomány részletek", "label.portable.ip.ranges": "Hordozható IP tartományok", "label.portable.ips": "Hordozható IP címek", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway felhasználónév", + "label.powerflex.gateway.password": "Átjáró jelszava", + "label.powerflex.storage.pool": "Tároló medence", "label.powerstate": "Power State", "label.prev": "Előző", "label.previous": "Előző", diff --git a/ui/l10n/it_IT.js b/ui/l10n/it_IT.js index 4c3ed120887..bdcf39e8eca 100644 --- a/ui/l10n/it_IT.js +++ b/ui/l10n/it_IT.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Il tuo nome internet non può essere risolto.", "force.delete": "Forza la Cancellazione", "force.delete.domain.warning": "Attenzione: La scelta di questa opzione provocherà la rimozione di tutti i sotto domini e agli account associati e alle loro risorse.", + "force.reboot":"Forza riavvio", "force.remove": "Forza la Rimozione", "force.remove.host.warning": "Attenzione: La scelta di questa opzione provocherà l'arresto forzato di tutte le virtual machine da parte di CloudStack prima di rimuovere questo host dal cluster.", "force.stop": "Forza l'Arresto", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Power State", "label.prev": "Prev", "label.previous": "Precedente", diff --git a/ui/l10n/ja_JP.js b/ui/l10n/ja_JP.js index b876d334629..23be15e2f8a 100644 --- a/ui/l10n/ja_JP.js +++ b/ui/l10n/ja_JP.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "インターネット名を解決できません。", "force.delete": "強制的に削除する", "force.delete.domain.warning": "警告: このオプションを選択すると、すべての子ドメインおよび関連するすべてのアカウントとそのリソースが削除されます。", + "force.reboot":"強制再起動", "force.remove": "強制的に解除する", "force.remove.host.warning": "警告: このオプションを選択すると、実行中のすべての仮想マシンが強制的に停止され、クラスターからこのホストが強制的に解除されます。", "force.stop": "強制的に停止する", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "ポータブル IP アドレスの範囲の詳細", "label.portable.ip.ranges": "ポータブル IP アドレスの範囲", "label.portable.ips": "ポータブル IP アドレス", + "label.powerflex.gateway": "ゲートウェイ", + "label.powerflex.gateway.username": "ゲートウェイユーザー名", + "label.powerflex.gateway.password": "ゲートウェイパスワード", + "label.powerflex.storage.pool": "ストレージプール", "label.powerstate": "Power State", "label.prev": "戻る", "label.previous": "戻る", diff --git a/ui/l10n/ko_KR.js b/ui/l10n/ko_KR.js index bdcae7d5a0e..a27a5aac4b3 100644 --- a/ui/l10n/ko_KR.js +++ b/ui/l10n/ko_KR.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "인터넷 주소를 알수 없습니다.", "force.delete": "강제 삭제", "force.delete.domain.warning": "경고:이 옵션을 선택하면, 모든 내부 도메인 및 관련하는 모든 계정 정보와 그 자원이 삭제됩니다.", + "force.reboot":"강제 재부팅", "force.remove": "강제 해제", "force.remove.host.warning": "경고:이 옵션을 선택하면, 실행중 모든 가상 머신이 강제적으로 정지되어 클러스터에서 호스트가 강제적으로 해제됩니다.", "force.stop": "강제 정지", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "게이트웨이", + "label.powerflex.gateway.username": "게이트웨이 사용자 이름", + "label.powerflex.gateway.password": "게이트웨이 비밀번호", + "label.powerflex.storage.pool": "스토리지 풀", "label.powerstate": "Power State", "label.prev": "뒤로", "label.previous": "뒤로", diff --git a/ui/l10n/nb_NO.js b/ui/l10n/nb_NO.js index 364a3fc14a7..a4c6c0f94bb 100644 --- a/ui/l10n/nb_NO.js +++ b/ui/l10n/nb_NO.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Ditt internettnavn kan ikke løses.", "force.delete": "Tving sletting", "force.delete.domain.warning": "Advarsel: dette alternativet vil medføre at alle underdomener og alle assosierte kontoer og dere resurser blir slettet.", + "force.reboot":"Tving omstart", "force.remove": "Tving fjerning", "force.remove.host.warning": "Advarsel: ved valg av dette alternativet vil CloudStack stoppe alle kjørende virtuelle maskiner, før verten blir fjernet fra klyngen.", "force.stop": "Tving stopp", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portabel IP-rekke detaljer", "label.portable.ip.ranges": "Transportable IP-rekker", "label.portable.ips": "Portabel IP-rekke", + "label.powerflex.gateway": "Inngangsport", + "label.powerflex.gateway.username": "Gateway brukernavn", + "label.powerflex.gateway.password": "Gateway-passord", + "label.powerflex.storage.pool": "Oppbevaringsbasseng", "label.powerstate": "Power State", "label.prev": "Forrige", "label.previous": "Forrige", diff --git a/ui/l10n/nl_NL.js b/ui/l10n/nl_NL.js index ffb1f1e5030..2e9f512fb11 100644 --- a/ui/l10n/nl_NL.js +++ b/ui/l10n/nl_NL.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Uw internet naam kan niet worden omgezet.", "force.delete": "Geforceerd verwijderen", "force.delete.domain.warning": "Waarschuwing: Wanneer u deze optie selecteert zullen alle onderliggende domeinen, hun gekoppelde accounts en hun verbruik worden verwijderd.", + "force.reboot":"Forceer opnieuw opstarten", "force.remove": "Geforceerd loskoppelen", "force.remove.host.warning": "Waarschuwing: Wanneer u deze optie selecteert zal CloudStack alle draaiende virtuele machines geforceerd stoppen voordat de host van het cluster wordt verwijderd.", "force.stop": "Geforceerd stoppen", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Porteerbare IP Range details", "label.portable.ip.ranges": "Porteerbare IP Ranges", "label.portable.ips": "Porteerbare IPs", + "label.powerflex.gateway": "poort", + "label.powerflex.gateway.username": "Gateway gebruikersnaam", + "label.powerflex.gateway.password": "Gateway-wachtwoord", + "label.powerflex.storage.pool": "Opslagpool", "label.powerstate": "Power State", "label.prev": "Terug", "label.previous": "Vorige", diff --git a/ui/l10n/pl.js b/ui/l10n/pl.js index 7f993c66803..f1090eb1509 100644 --- a/ui/l10n/pl.js +++ b/ui/l10n/pl.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Your internet name cannot be resolved.", "force.delete": "Force Delete", "force.delete.domain.warning": "Warning: Choosing this option will cause the deletion of all child domains and all associated accounts and their resources.", + "force.reboot":"Force Reboot", "force.remove": "Force Remove", "force.remove.host.warning": "Warning: Choosing this option will cause CloudStack to forcefully stop all running virtual machines before removing this host from the cluster.", "force.stop": "Force Stop", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Gateway", + "label.powerflex.gateway.username": "Gateway Username", + "label.powerflex.gateway.password": "Gateway Password", + "label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Power State", "label.prev": "Prev", "label.previous": "Wstecz", diff --git a/ui/l10n/pt_BR.js b/ui/l10n/pt_BR.js index d3969a3dd48..086f8a41f9e 100644 --- a/ui/l10n/pt_BR.js +++ b/ui/l10n/pt_BR.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Impossível resolver DNS", "force.delete": "Forçar Exclusão", "force.delete.domain.warning": "Atenção: Esta opção removerá todos os domínios, contas e recursos associados.", + "force.reboot":"Forçar reinicialização", "force.remove": "Forçar Remoção", "force.remove.host.warning": "Atenção: O CloudStack desligará de maneira forçada todas as VMs antes de remover o host do cluster.", "force.stop": "Forçar Parada", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Detalhes de Range de IP Portáveis", "label.portable.ip.ranges": "Faixa de endereços IPs Portável", "label.portable.ips": "IPs Portáveis", + "label.powerflex.gateway": "Porta de entrada", + "label.powerflex.gateway.username": "Nome de usuário do gateway", + "label.powerflex.gateway.password": "Senha de gateway", + "label.powerflex.storage.pool": "Pool de armazenamento", "label.powerstate": "Power State", "label.prev": "Prev", "label.previous": "Anterior", diff --git a/ui/l10n/ru_RU.js b/ui/l10n/ru_RU.js index 49f4d8ae47b..482b4b2d040 100644 --- a/ui/l10n/ru_RU.js +++ b/ui/l10n/ru_RU.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "Ваше сетевое имя не удалось разрешить.", "force.delete": "Принудительное удаление", "force.delete.domain.warning": "Предупреждение: Выбор этой опции приведет к удалению всех дочерних доменов и связанных с ними учетных записей и их ресурсов", + "force.reboot":"Принудительная перезагрузка", "force.remove": "Принудительное удаление", "force.remove.host.warning": "Выбор этой опции приведет к принудительной остановке работающих виртуальных машин перед удалением сервера из кластера.", "force.stop": "Принудительно остановить", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "Portable IP Range details", "label.portable.ip.ranges": "Portable IP Ranges", "label.portable.ips": "Portable IPs", + "label.powerflex.gateway": "Шлюз", + "label.powerflex.gateway.username": "Имя пользователя шлюза", + "label.powerflex.gateway.password": "Пароль шлюза", + "label.powerflex.storage.pool": "Пул хранения", "label.powerstate": "Power State", "label.prev": "Предыдуший", "label.previous": "Предыдущий", diff --git a/ui/l10n/zh_CN.js b/ui/l10n/zh_CN.js index 26cb7081869..76eaf440fae 100644 --- a/ui/l10n/zh_CN.js +++ b/ui/l10n/zh_CN.js @@ -37,6 +37,7 @@ var dictionary = { "error.unresolved.internet.name": "无法解析您的 Internet 名称。", "force.delete": "强制删除", "force.delete.domain.warning": "警告: 选择此选项将导致删除所有子域以及所有相关联的帐户及其资源。", + "force.reboot":"強制重啟", "force.remove": "强制移除", "force.remove.host.warning": "警告: 选择此选项将导致 CloudStack 在从群集中移除此主机之前,强制停止所有正在运行的虚拟机。", "force.stop": "强制停止", @@ -1261,6 +1262,10 @@ var dictionary = { "label.portable.ip.range.details": "可移植 IP 范围详细信息", "label.portable.ip.ranges": "可移植 IP 范围", "label.portable.ips": "可移植 IP", + "label.powerflex.gateway": "網關", + "label.powerflex.gateway.username": "網關用戶名", + "label.powerflex.gateway.password": "網關密碼", + "label.powerflex.storage.pool": "儲存池", "label.powerstate": "Power State", "label.prev": "上一页", "label.previous": "上一步", diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js index 7f29f2b3ac2..0ccf00a9e74 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -734,6 +734,22 @@ cloudStack.docs = { desc: 'In iSCSI, this is the LUN number. For example, 3.', externalLink: '' }, + helpPrimaryStoragePowerFlexGateway: { + desc: 'The address of PowerFlex Gateway host', + externalLink: '' + }, + helpPrimaryStoragePowerFlexGatewayUsername: { + desc: 'Username of PowerFlex Gateway for API access', + externalLink: '' + }, + helpPrimaryStoragePowerFlexGatewayPassword: { + desc: 'Password of PowerFlex Gateway for API access', + externalLink: '' + }, + helpPrimaryStoragePowerFlexStoragePool: { + desc: 'Storage pool on PowerFlex', + externalLink: '' + }, helpPrimaryStorageRBDMonitor: { desc: 'The address of a Ceph monitor. Can also be a Round Robin DNS record', externalLink: '' diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index de71a206b33..f4a9c7052d6 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -137,6 +137,91 @@ return action; }; + var vmRestartAction = function(args) { + var action = { + messages: { + confirm: function(args) { + return 'message.action.reboot.instance'; + }, + notification: function(args) { + return 'label.action.reboot.instance'; + }, + complete: function(args) { + if (args.password != null && args.password.length > 0) + return _l('message.password.has.been.reset.to') + ' ' + args.password; + else + return null; + } + }, + label: 'label.action.reboot.instance', + compactLabel: 'label.reboot', + addRow: 'false', + createForm: { + title: 'notification.reboot.instance', + desc: 'message.action.reboot.instance', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, + action: function(args) { + var instances = args.context.instances; + var skippedInstances = 0; + $(instances).each(function(index, instance) { + if (instance.state === 'Stopped' || instance.state === 'Stopping') { + skippedInstances++; + } else { + var data = { + id: instance.id, + forced: (args.data.forced == "on") + }; + $.ajax({ + url: createURL("rebootVirtualMachine"), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jid = json.rebootvirtualmachineresponse.jobid; + args.response.success({ + _custom: { + jobId: jid, + getUpdatedItem: function(json) { + return json.queryasyncjobresultresponse.jobresult.virtualmachine; + }, + getActionFilter: function() { + return vmActionfilter; + } + } + }); + }, + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + } + }); + if (skippedInstances === instances.length) { + args.response.error(); + } + }, + notification: { + poll: pollAsyncJobResult + } + }; + + if (args && args.listView) { + $.extend(action, { + isHeader: true, + isMultiSelectAction: true + }); + } + + return action; + }; + var vmStopAction = function(args) { var action = { messages: { @@ -1112,48 +1197,7 @@ } }, stop: vmStopAction(), - restart: { - label: 'label.action.reboot.instance', - compactLabel: 'label.reboot', - action: function(args) { - $.ajax({ - url: createURL("rebootVirtualMachine&id=" + args.context.instances[0].id), - dataType: "json", - async: true, - success: function(json) { - var jid = json.rebootvirtualmachineresponse.jobid; - args.response.success({ - _custom: { - jobId: jid, - getUpdatedItem: function(json) { - return json.queryasyncjobresultresponse.jobresult.virtualmachine; - }, - getActionFilter: function() { - return vmActionfilter; - } - } - }); - } - }); - }, - messages: { - confirm: function(args) { - return 'message.action.reboot.instance'; - }, - notification: function(args) { - return 'label.action.reboot.instance'; - }, - complete: function(args) { - if (args.password != null && args.password.length > 0) - return _l('message.password.has.been.reset.to') + ' ' + args.password; - else - return null; - } - }, - notification: { - poll: pollAsyncJobResult - } - }, + restart: vmRestartAction(), snapshot: vmSnapshotAction(), storageSnapshot: { messages: { @@ -1219,7 +1263,18 @@ }, asyncBackup: { label: 'label.async.backup', - isBoolean: true + isBoolean: true, + dependsOn: 'volume', + isHidden: function(args) { + var selectedVolumeId = $('div[role=dialog] form .form-item[rel=volume] select').val(); + for (var i = 0; i < args.context.volumes.length; i++) { + var volume = args.context.volumes[i]; + if (volume.id === selectedVolumeId) { + return volume.supportsstoragesnapshot === true; + } + } + return false; + } } } }, diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index c13af41deb8..b1444a40d43 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -1878,6 +1878,10 @@ var processPropertiesInImagestoreObject = function(jsonObj) { return url; } + function powerflexURL(gateway, username, password, storagepool) { + var url = "powerflex://" + username + ":" + password + "@" + gateway + "/" + storagepool; + return url; + } //VM Instance diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index aa355ef5afa..b1022fb5a45 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -945,7 +945,13 @@ }, asyncBackup: { label: 'label.async.backup', - isBoolean: true + isBoolean: true, + isHidden: function(args) { + if (args.context.volumes[0].supportsstoragesnapshot == true) + return true; + else + return false; + } }, tags: { label: 'label.tags', diff --git a/ui/scripts/system.js b/ui/scripts/system.js index 29f428a4f02..0595359c59b 100755 --- a/ui/scripts/system.js +++ b/ui/scripts/system.js @@ -3793,6 +3793,17 @@ restart: { label: 'label.action.reboot.router', + createForm: { + title: 'label.action.reboot.router', + desc: 'message.action.reboot.router', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, messages: { confirm: function (args) { return 'message.action.reboot.router'; @@ -3802,8 +3813,10 @@ } }, action: function (args) { + var array1 =[]; + array1.push("&forced=" + (args.data.forced == "on")); $.ajax({ - url: createURL('rebootRouter&id=' + args.context.routers[0].id), + url: createURL('rebootRouter&id=' + args.context.routers[0].id + array1.join("")), dataType: 'json', async: true, success: function (json) { @@ -8809,6 +8822,17 @@ restart: { label: 'label.action.reboot.systemvm', + createForm: { + title: 'label.action.reboot.systemvm', + desc: 'message.action.reboot.systemvm', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, messages: { confirm: function (args) { return 'message.action.reboot.systemvm'; @@ -8818,8 +8842,10 @@ } }, action: function (args) { + var array1 =[]; + array1.push("&forced=" + (args.data.forced == "on")); $.ajax({ - url: createURL('rebootSystemVm&id=' + args.context.systemVMs[0].id), + url: createURL('rebootSystemVm&id=' + args.context.systemVMs[0].id + array1.join("")), dataType: 'json', async: true, success: function (json) { @@ -10340,6 +10366,17 @@ restart: { label: 'label.action.reboot.router', + createForm: { + title: 'label.action.reboot.router', + desc: 'message.action.reboot.router', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, messages: { confirm: function (args) { return 'message.action.reboot.router'; @@ -10349,8 +10386,10 @@ } }, action: function (args) { + var array1 =[]; + array1.push("&forced=" + (args.data.forced == "on")); $.ajax({ - url: createURL('rebootRouter&id=' + args.context.routers[0].id), + url: createURL('rebootRouter&id=' + args.context.routers[0].id + array1.join("")), dataType: 'json', async: true, success: function (json) { @@ -11826,6 +11865,17 @@ restart: { label: 'label.action.reboot.systemvm', + createForm: { + title: 'label.action.reboot.systemvm', + desc: 'message.action.reboot.systemvm', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, messages: { confirm: function (args) { return 'message.action.reboot.systemvm'; @@ -11835,8 +11885,10 @@ } }, action: function (args) { + var array1 =[]; + array1.push("&forced=" + (args.data.forced == "on")); $.ajax({ - url: createURL('rebootSystemVm&id=' + args.context.systemVMs[0].id), + url: createURL('rebootSystemVm&id=' + args.context.systemVMs[0].id + array1.join("")), dataType: 'json', async: true, success: function (json) { @@ -19128,6 +19180,7 @@ }, provider: { label: 'label.provider', + dependsOn: 'protocol', validation: { required: true }, @@ -19136,6 +19189,8 @@ { id: args.context.providers[0].id } : {}; + var items =[]; + $.ajax({ url: createURL('listStorageProviders'), data: { @@ -19146,10 +19201,19 @@ args.response.success({ data: $.map(providers, function (provider) { - return { - id: provider.name, - description: provider.name - }; + if (args.protocol != "custom") { + if (provider.name != "PowerFlex") { + return { + id: provider.name, + description: provider.name + }; + } + } else { + return { + id: provider.name, + description: provider.name + }; + } }) }); } @@ -19163,11 +19227,31 @@ $form.find('.form-item[rel=capacityIops]').hide(); $form.find('.form-item[rel=capacityBytes]').hide(); $form.find('.form-item[rel=url]').hide(); + + $form.find('.form-item[rel=powerflexGateway]').hide(); + $form.find('.form-item[rel=powerflexGatewayUsername]').hide();; + $form.find('.form-item[rel=powerflexGatewayPassword]').hide();; + $form.find('.form-item[rel=powerflexStoragePool]').hide();; + } else if (scope == 'PowerFlex') { + $form.find('.form-item[rel=isManaged]').hide(); + $form.find('.form-item[rel=capacityIops]').hide(); + $form.find('.form-item[rel=capacityBytes]').hide(); + $form.find('.form-item[rel=url]').hide(); + + $form.find('.form-item[rel=powerflexGateway]').css('display', 'inline-block'); + $form.find('.form-item[rel=powerflexGatewayUsername]').css('display', 'inline-block'); + $form.find('.form-item[rel=powerflexGatewayPassword]').css('display', 'inline-block'); + $form.find('.form-item[rel=powerflexStoragePool]').css('display', 'inline-block'); } else { $form.find('.form-item[rel=isManaged]').css('display', 'inline-block'); $form.find('.form-item[rel=capacityIops]').css('display', 'inline-block'); $form.find('.form-item[rel=capacityBytes]').css('display', 'inline-block'); $form.find('.form-item[rel=url]').css('display', 'inline-block'); + + $form.find('.form-item[rel=powerflexGateway]').hide(); + $form.find('.form-item[rel=powerflexGatewayUsername]').hide();; + $form.find('.form-item[rel=powerflexGatewayPassword]').hide();; + $form.find('.form-item[rel=powerflexStoragePool]').hide();; } } ) @@ -19312,6 +19396,41 @@ isHidden: true }, + // PowerFlex/ScaleIO + powerflexGateway: { + label: 'label.powerflex.gateway', + docID: 'helpPrimaryStoragePowerFlexGateway', + validation: { + required: true + }, + isHidden: true + }, + powerflexGatewayUsername: { + label: 'label.powerflex.gateway.username', + docID: 'helpPrimaryStoragePowerFlexGatewayUsername', + validation: { + required: true + }, + isHidden: true + }, + powerflexGatewayPassword: { + label: 'label.powerflex.gateway.password', + docID: 'helpPrimaryStoragePowerFlexGatewayPassword', + isPassword: true, + validation: { + required: true + }, + isHidden: true + }, + powerflexStoragePool: { + label: 'label.powerflex.storage.pool', + docID: 'helpPrimaryStoragePowerFlexStoragePool', + validation: { + required: true + }, + isHidden: true + }, + //always appear (begin) storageTags: { label: 'label.storage.tags', @@ -19450,9 +19569,16 @@ } array1.push("&url=" + encodeURIComponent(url)); - } - else - { + } else if (args.data.provider == "PowerFlex") { + var url = null; + var gateway = args.data.powerflexGateway; + var gwusername = encodeURIComponent(args.data.powerflexGatewayUsername); + var gwpassword = encodeURIComponent(args.data.powerflexGatewayPassword); + var storagepool = encodeURIComponent(args.data.powerflexStoragePool); + url = powerflexURL(gateway, gwusername, gwpassword, storagepool); + + array1.push("&url=" + encodeURIComponent(url)); + } else { array1.push("&managed=" + (args.data.isManaged == "on").toString()); if (args.data.capacityBytes != null && args.data.capacityBytes.length > 0) diff --git a/ui/scripts/vpc.js b/ui/scripts/vpc.js index c340c18ecf0..b1e5541d8b5 100644 --- a/ui/scripts/vpc.js +++ b/ui/scripts/vpc.js @@ -1726,9 +1726,23 @@ }, restart: { label: 'instances.actions.reboot.label', + addRow: 'false', + createForm: { + title: 'label.action.reboot.instance', + desc: 'message.action.reboot.instance', + fields: { + forced: { + label: 'force.reboot', + isBoolean: true, + isChecked: false + } + } + }, action: function(args) { + var array1 = []; + array1.push("&forced=" + (args.data.forced == "on")); $.ajax({ - url: createURL("rebootVirtualMachine&id=" + args.context.vpcTierInstances[0].id), + url: createURL("rebootVirtualMachine&id=" + args.context.vpcTierInstances[0].id + array1.join("")), dataType: "json", async: true, success: function(json) {