This is an automated email from the ASF dual-hosted git repository. sureshanaparti pushed a commit to branch 4.19 in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.19 by this push: new 063dc601140 Change storage pool scope from Cluster to Zone and vise versa (#8875) 063dc601140 is described below commit 063dc6011403101c10382d3e950951c96b97755c Author: Abhisar Sinha <63767682+abh1...@users.noreply.github.com> AuthorDate: Sat Jun 29 10:03:34 2024 +0530 Change storage pool scope from Cluster to Zone and vise versa (#8875) * New feature: Change storage pool scope * Added checks for Ceph/RBD * Update op_host_capacity table on primary storage scope change * Storage pool scope change integration test * pull 8875 : Addressed review comments * Pull 8875: remove storage checks, AbstractPrimayStorageLifeCycleImpl class * Pull 8875: Fixed integration test failure * Pull 8875: Review comments * Pull 8875: review comments + broke changeStoragePoolScope into smaller functions * Added UT for changeStoragePoolScope * Rename AbstractPrimaryDataStoreLifeCycleImpl to BasePrimaryDataStoreLifeCycleImpl * Pull 8875: Dao review comments * Pull 8875: Rename changeStoragePoolScope.vue to ChangeStoragePoolScope.vue * Pull 8875: Created a new smokes test file + A single warning msg in ui * Pull 8875: Added cleanup in test_primary_storage_scope.py * Pull 8875: Type in en.json * Pull 8875: cleanup array in test_primary_storage_scope.py * Pull:8875 Removing extra whitespace at eof of StorageManagerImplTest * Pull 8875: Added UT for PrimaryDataStoreHelper and BasePrimaryDataStoreLifeCycleImpl * Pull 8875: Added license header * Pull 8875: Fixed sql query for vmstates * Pull 8875: Changed icon plus info on disabled mode in apidoc * Pull 8875: Change scope should not work for local storage * Pull 8875: Change scope completion event * Pull 8875: Added api findAffectedVmsForStorageScopeChange * Pull 8875: Added UT for findAffectedVmsForStorageScopeChange and removed listByPoolIdVMStatesNotInCluster * Pull 8875: Review comments + Vm name in response * Pull 8875: listByVmsNotInClusterUsingPool was returning duplicate VM entries because of multiple volumes in the VM satisfying the criteria * Pull 8875: fixed listAffectedVmsForStorageScopeChange UT * listAffectedVmsForStorageScopeChange should work if the pool is not disabled * Fix listAffectedVmsForStorageScopeChangeTest UT * Pull 8875: add volume.removed not null check in VmsNotInClusterUsingPool query * Pull 8875: minor refactoring in changeStoragePoolScopeToCluster * Update server/src/main/java/com/cloud/storage/StorageManagerImpl.java * fix eof * changeStoragePoolScopeToZone should connect pool to all Up hosts Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anapa...@gmail.com> --- api/src/main/java/com/cloud/event/EventTypes.java | 2 + .../java/com/cloud/storage/StorageService.java | 4 + .../admin/storage/ChangeStoragePoolScopeCmd.java | 98 +++++++++ .../ListAffectedVmsForStorageScopeChangeCmd.java | 77 +++++++ .../api/response/VirtualMachineResponse.java | 124 ++++++++++++ .../org/apache/cloudstack/query/QueryService.java | 4 + .../api/storage/PrimaryDataStoreLifeCycle.java | 3 + .../java/com/cloud/resource/ResourceManager.java | 4 + .../main/java/com/cloud/capacity/CapacityVO.java | 8 +- .../com/cloud/storage/dao/StoragePoolHostDao.java | 2 + .../cloud/storage/dao/StoragePoolHostDaoImpl.java | 28 +++ .../java/com/cloud/storage/dao/VolumeDaoImpl.java | 8 +- .../main/java/com/cloud/vm/dao/VMInstanceDao.java | 2 + .../java/com/cloud/vm/dao/VMInstanceDaoImpl.java | 39 +++- .../volume/datastore/PrimaryDataStoreHelper.java | 57 +++++- .../datastore/PrimaryDataStoreHelperTest.java | 114 +++++++++++ .../BasePrimaryDataStoreLifeCycleImpl.java | 106 ++++++++++ .../BasePrimaryDataStoreLifeCycleImplTest.java | 127 ++++++++++++ .../lifecycle/AdaptiveDataStoreLifeCycleImpl.java | 2 +- .../ElastistorPrimaryDataStoreLifeCycle.java | 2 +- .../lifecycle/DateraPrimaryDataStoreLifeCycle.java | 17 +- .../CloudStackPrimaryDataStoreLifeCycleImpl.java | 2 +- .../LinstorPrimaryDataStoreLifeCycleImpl.java | 2 +- .../NexentaPrimaryDataStoreLifeCycle.java | 11 + .../SamplePrimaryDataStoreLifeCycleImpl.java | 8 + .../ScaleIOPrimaryDataStoreLifeCycle.java | 2 +- .../SolidFirePrimaryDataStoreLifeCycle.java | 11 +- .../SolidFireSharedPrimaryDataStoreLifeCycle.java | 2 +- .../StorPoolPrimaryDataStoreLifeCycle.java | 2 +- .../java/com/cloud/api/query/QueryManagerImpl.java | 60 ++++++ .../com/cloud/resource/ResourceManagerImpl.java | 20 ++ .../com/cloud/server/ManagementServerImpl.java | 4 + .../java/com/cloud/storage/StorageManagerImpl.java | 118 ++++++++++- .../com/cloud/api/query/QueryManagerImplTest.java | 72 +++++++ .../cloud/resource/MockResourceManagerImpl.java | 11 + .../com/cloud/storage/StorageManagerImplTest.java | 111 ++++++++-- .../smoke/test_primary_storage_scope.py | 176 ++++++++++++++++ tools/apidoc/gen_toc.py | 1 + ui/public/locales/en.json | 7 + ui/src/config/section/infra/primaryStorages.js | 20 ++ ui/src/core/lazy_lib/icons_use.js | 2 + ui/src/views/infra/ChangeStoragePoolScope.vue | 223 +++++++++++++++++++++ 42 files changed, 1642 insertions(+), 51 deletions(-) diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 689676290b3..496d9f5b689 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -451,6 +451,7 @@ public class EventTypes { public static final String EVENT_ENABLE_PRIMARY_STORAGE = "ENABLE.PS"; public static final String EVENT_DISABLE_PRIMARY_STORAGE = "DISABLE.PS"; public static final String EVENT_SYNC_STORAGE_POOL = "SYNC.STORAGE.POOL"; + public static final String EVENT_CHANGE_STORAGE_POOL_SCOPE = "CHANGE.STORAGE.POOL.SCOPE"; // VPN public static final String EVENT_REMOTE_ACCESS_VPN_CREATE = "VPN.REMOTE.ACCESS.CREATE"; @@ -1000,6 +1001,7 @@ public class EventTypes { // Primary storage pool entityEventDetails.put(EVENT_ENABLE_PRIMARY_STORAGE, StoragePool.class); entityEventDetails.put(EVENT_DISABLE_PRIMARY_STORAGE, StoragePool.class); + entityEventDetails.put(EVENT_CHANGE_STORAGE_POOL_SCOPE, StoragePool.class); // VPN entityEventDetails.put(EVENT_REMOTE_ACCESS_VPN_CREATE, RemoteAccessVpn.class); diff --git a/api/src/main/java/com/cloud/storage/StorageService.java b/api/src/main/java/com/cloud/storage/StorageService.java index 77800d8955c..1ce335b0115 100644 --- a/api/src/main/java/com/cloud/storage/StorageService.java +++ b/api/src/main/java/com/cloud/storage/StorageService.java @@ -21,6 +21,7 @@ import java.net.UnknownHostException; import java.util.Map; import org.apache.cloudstack.api.command.admin.storage.CancelPrimaryStorageMaintenanceCmd; +import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; import org.apache.cloudstack.api.command.admin.storage.CreateSecondaryStagingStoreCmd; import org.apache.cloudstack.api.command.admin.storage.CreateStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.DeleteImageStoreCmd; @@ -35,6 +36,7 @@ import org.apache.cloudstack.api.command.admin.storage.UpdateStoragePoolCmd; import com.cloud.exception.DiscoveryException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceInUseException; import com.cloud.exception.ResourceUnavailableException; import org.apache.cloudstack.api.command.admin.storage.heuristics.CreateSecondaryStorageSelectorCmd; @@ -130,4 +132,6 @@ public interface StorageService { boolean deleteObjectStore(DeleteObjectStoragePoolCmd cmd); ObjectStore updateObjectStore(Long id, UpdateObjectStoragePoolCmd cmd); + + void changeStoragePoolScope(ChangeStoragePoolScopeCmd cmd) throws IllegalArgumentException, InvalidParameterValueException, PermissionDeniedException; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java new file mode 100644 index 00000000000..d3b6a074610 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java @@ -0,0 +1,98 @@ +// 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.api.command.admin.storage; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.storage.StoragePool; + +@APICommand(name = "changeStoragePoolScope", description = "Changes the scope of a storage pool when the pool is in Disabled state." + + "This feature is officially tested and supported for Hypervisors: KVM and VMware, Protocols: NFS and Ceph, and Storage Provider: DefaultPrimary. " + + "There might be extra steps involved to make this work for other hypervisors and storage options.", + responseObject = SuccessResponse.class, since= "4.19.1", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ChangeStoragePoolScopeCmd extends BaseAsyncCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = StoragePoolResponse.class, required = true, description = "the Id of the storage pool") + private Long id; + + @Parameter(name = ApiConstants.SCOPE, type = CommandType.STRING, required = true, description = "the scope of the storage: cluster or zone") + private String scope; + + @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "the Id of the cluster to use if scope is being set to Cluster") + private Long clusterId; + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.StoragePool; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + public String getEventType() { + return EventTypes.EVENT_CHANGE_STORAGE_POOL_SCOPE; + } + + @Override + public String getEventDescription() { + String description = "Change storage pool scope. Storage pool Id: "; + StoragePool pool = _entityMgr.findById(StoragePool.class, getId()); + if (pool != null) { + description += pool.getUuid(); + } else { + description += getId(); + } + description += " to " + getScope(); + return description; + } + + @Override + public void execute() { + _storageService.changeStoragePoolScope(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + public Long getId() { + return id; + } + + public String getScope() { + return scope; + } + + public Long getClusterId() { + return clusterId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListAffectedVmsForStorageScopeChangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListAffectedVmsForStorageScopeChangeCmd.java new file mode 100644 index 00000000000..d586a81b685 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListAffectedVmsForStorageScopeChangeCmd.java @@ -0,0 +1,77 @@ +/* + * 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.api.command.admin.vm; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; + +import com.cloud.vm.VirtualMachine; + +@APICommand(name = "listAffectedVmsForStorageScopeChange", + description = "List user and system VMs that need to be stopped and destroyed respectively for changing the scope of the storage pool from Zone to Cluster.", + responseObject = VirtualMachineResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.1", + authorized = {RoleType.Admin}) +public class ListAffectedVmsForStorageScopeChangeCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.CLUSTER_ID, + type = CommandType.UUID, + entityType = ClusterResponse.class, + required = true, + description = "the Id of the cluster the scope of the storage pool is being changed to") + private Long clusterIdForScopeChange; + + @Parameter(name = ApiConstants.STORAGE_ID, + type = CommandType.UUID, + entityType = StoragePoolResponse.class, + required = true, + description = "the Id of the storage pool on which change scope operation is being done") + private Long storageId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getClusterIdForScopeChange() { + return clusterIdForScopeChange; + } + + public Long getStorageId() { + return storageId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ListResponse<VirtualMachineResponse> response = _queryService.listAffectedVmsForStorageScopeChange(this); + response.setResponseName(getCommandName()); + response.setObjectName(VirtualMachine.class.getSimpleName().toLowerCase()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VirtualMachineResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VirtualMachineResponse.java new file mode 100644 index 00000000000..7d676292b8a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VirtualMachineResponse.java @@ -0,0 +1,124 @@ +// 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.api.response; + +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import com.cloud.serializer.Param; +import com.cloud.vm.VirtualMachine; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = VirtualMachine.class) +public class VirtualMachineResponse extends BaseResponse { + @SerializedName("id") + @Param(description = "the ID of the VM") + private String id; + + @SerializedName("type") + @Param(description = "the type of VM") + private String type; + + @SerializedName("name") + @Param(description = "the name of the VM") + private String name; + + @SerializedName("clusterid") + @Param(description = "the cluster ID for the VM") + private String clusterId; + + @SerializedName("clustername") + @Param(description = "the cluster name for the VM") + private String clusterName; + + @SerializedName("hostid") + @Param(description = "the host ID for the VM") + private String hostId; + + @SerializedName("hostname") + @Param(description = "the hostname for the VM") + private String hostName; + + @Override + public String getObjectId() { + return this.getId(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getVmType() { + return type; + } + + public void setVmType(String type) { + this.type = type; + } + + public String getVmName() { + return name; + } + + public void setVmName(String name) { + this.name = name; + } + + public String getClusterId() { + return clusterId; + } + + public void setClusterId(String clusterId) { + this.clusterId = clusterId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHostId() { + return hostId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 4c53314aef5..c93e43d9f37 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -52,6 +52,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; +import org.apache.cloudstack.api.command.admin.vm.ListAffectedVmsForStorageScopeChangeCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; import org.apache.cloudstack.api.command.user.volume.ListResourceDetailsCmd; @@ -89,6 +90,7 @@ import org.apache.cloudstack.api.response.StorageTagResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.framework.config.ConfigKey; @@ -140,6 +142,8 @@ public interface QueryService { ListResponse<UserVmResponse> searchForUserVMs(ListVMsCmd cmd); + ListResponse<VirtualMachineResponse> listAffectedVmsForStorageScopeChange(ListAffectedVmsForStorageScopeChangeCmd cmd); + ListResponse<SecurityGroupResponse> searchForSecurityGroups(ListSecurityGroupsCmd cmd); ListResponse<DomainRouterResponse> searchForRouters(ListRoutersCmd cmd); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreLifeCycle.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreLifeCycle.java index fcbc19c28b7..54f3c63f8d7 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreLifeCycle.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreLifeCycle.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.engine.subsystem.api.storage; import java.util.Map; +import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.StoragePool; public interface PrimaryDataStoreLifeCycle extends DataStoreLifeCycle { @@ -29,4 +30,6 @@ public interface PrimaryDataStoreLifeCycle extends DataStoreLifeCycle { void updateStoragePool(StoragePool storagePool, Map<String, String> details); void enableStoragePool(DataStore store); void disableStoragePool(DataStore store); + void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, Hypervisor.HypervisorType hypervisorType); + void changeStoragePoolScopeToCluster(DataStore store, ClusterScope clusterScope, Hypervisor.HypervisorType hypervisorType); } diff --git a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java index 91197de6a84..b2ae8b89837 100755 --- a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java +++ b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java @@ -134,6 +134,10 @@ public interface ResourceManager extends ResourceService, Configurable { public List<HostVO> listAllHostsInAllZonesByType(Type type); + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisor(final HypervisorType type, long dcId, long clusterId); + + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisors(List<HypervisorType> types, long dcId, long clusterId); + public List<HypervisorType> listAvailHypervisorInZone(Long hostId, Long zoneId); public HostVO findHostByGuid(String guid); diff --git a/engine/schema/src/main/java/com/cloud/capacity/CapacityVO.java b/engine/schema/src/main/java/com/cloud/capacity/CapacityVO.java index 50c40134a91..29b58ddccd4 100644 --- a/engine/schema/src/main/java/com/cloud/capacity/CapacityVO.java +++ b/engine/schema/src/main/java/com/cloud/capacity/CapacityVO.java @@ -132,8 +132,8 @@ public class CapacityVO implements Capacity { return podId; } - public void setPodId(long podId) { - this.podId = new Long(podId); + public void setPodId(Long podId) { + this.podId = podId; } @Override @@ -141,8 +141,8 @@ public class CapacityVO implements Capacity { return clusterId; } - public void setClusterId(long clusterId) { - this.clusterId = new Long(clusterId); + public void setClusterId(Long clusterId) { + this.clusterId = clusterId; } @Override 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 b099a6d6bdb..62ef5b7570d 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 @@ -41,4 +41,6 @@ public interface StoragePoolHostDao extends GenericDao<StoragePoolHostVO, Long> public void deleteStoragePoolHostDetails(long hostId, long poolId); List<StoragePoolHostVO> listByHostId(long hostId); + + Pair<List<StoragePoolHostVO>, Integer> listByPoolIdNotInCluster(long clusterId, long poolId); } 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 c27aeb0f652..03da0405142 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 @@ -23,13 +23,19 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import javax.inject.Inject; + import org.apache.log4j.Logger; import org.springframework.stereotype.Component; +import com.cloud.host.HostVO; import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; import com.cloud.storage.StoragePoolHostVO; import com.cloud.utils.Pair; import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.JoinBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; @@ -42,6 +48,11 @@ public class StoragePoolHostDaoImpl extends GenericDaoBase<StoragePoolHostVO, Lo protected final SearchBuilder<StoragePoolHostVO> HostSearch; protected final SearchBuilder<StoragePoolHostVO> PoolHostSearch; + protected SearchBuilder<StoragePoolHostVO> poolNotInClusterSearch; + + @Inject + HostDao hostDao; + protected static final String HOST_FOR_POOL_SEARCH = "SELECT * FROM storage_pool_host_ref ph, host h where ph.host_id = h.id and ph.pool_id=? and h.status=? "; protected static final String HOSTS_FOR_POOLS_SEARCH = "SELECT DISTINCT(ph.host_id) FROM storage_pool_host_ref ph, host h WHERE ph.host_id = h.id AND h.status = 'Up' AND resource_state = 'Enabled' AND ph.pool_id IN (?)"; @@ -70,6 +81,15 @@ public class StoragePoolHostDaoImpl extends GenericDaoBase<StoragePoolHostVO, Lo } + @PostConstruct + public void init(){ + poolNotInClusterSearch = createSearchBuilder(); + poolNotInClusterSearch.and("poolId", poolNotInClusterSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + SearchBuilder<HostVO> hostSearch = hostDao.createSearchBuilder(); + poolNotInClusterSearch.join("hostSearch", hostSearch, hostSearch.entity().getId(), poolNotInClusterSearch.entity().getHostId(), JoinBuilder.JoinType.INNER); + hostSearch.and("clusterId", hostSearch.entity().getClusterId(), SearchCriteria.Op.NEQ); + } + @Override public List<StoragePoolHostVO> listByPoolId(long id) { SearchCriteria<StoragePoolHostVO> sc = PoolSearch.create(); @@ -196,4 +216,12 @@ public class StoragePoolHostDaoImpl extends GenericDaoBase<StoragePoolHostVO, Lo remove(sc); txn.commit(); } + + @Override + public Pair<List<StoragePoolHostVO>, Integer> listByPoolIdNotInCluster(long clusterId, long poolId) { + SearchCriteria<StoragePoolHostVO> sc = poolNotInClusterSearch.create(); + sc.setParameters("poolId", poolId); + sc.setJoinParameters("hostSearch", "clusterId", clusterId); + return searchAndCount(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 3a3c02d83b4..9907af76769 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -72,8 +72,9 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol protected GenericSearchBuilder<VolumeVO, SumCount> primaryStorageSearch2; protected GenericSearchBuilder<VolumeVO, SumCount> secondaryStorageSearch; private final SearchBuilder<VolumeVO> poolAndPathSearch; + @Inject - ResourceTagDao _tagsDao; + ResourceTagDao tagsDao; // need to account for zone-wide primary storage where storage_pool has // null-value pod and cluster, where hypervisor information is stored in @@ -493,7 +494,6 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol poolAndPathSearch.and("poolId", poolAndPathSearch.entity().getPoolId(), Op.EQ); poolAndPathSearch.and("path", poolAndPathSearch.entity().getPath(), Op.EQ); poolAndPathSearch.done(); - } @Override @@ -719,7 +719,7 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol s_logger.debug(String.format("Removing volume %s from DB", id)); VolumeVO entry = findById(id); if (entry != null) { - _tagsDao.removeByIdAndType(id, ResourceObjectType.Volume); + tagsDao.removeByIdAndType(id, ResourceObjectType.Volume); } boolean result = super.remove(id); @@ -742,7 +742,7 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol destVol.setInstanceId(instanceId); update(srcVolId, srcVol); update(destVolId, destVol); - _tagsDao.updateResourceId(srcVolId, destVolId, ResourceObjectType.Volume); + tagsDao.updateResourceId(srcVolId, destVolId, ResourceObjectType.Volume); } catch (Exception e) { throw new CloudRuntimeException("Unable to persist the sequence number for this host"); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index 42c00231aac..63f7ad974e6 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -165,4 +165,6 @@ public interface VMInstanceDao extends GenericDao<VMInstanceVO, Long>, StateDao< void updateSystemVmTemplateId(long templateId, Hypervisor.HypervisorType hypervisorType); List<VMInstanceVO> listByHostOrLastHostOrHostPod(List<Long> hostIds, long podId); + + Pair<List<VMInstanceVO>, Integer> listByVmsNotInClusterUsingPool(long clusterId, long poolId); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 322895f0ec5..e41c706a9cb 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -24,6 +24,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.inject.Inject; @@ -35,6 +36,8 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.server.ResourceTag.ResourceObjectType; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; @@ -97,11 +100,16 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem protected SearchBuilder<VMInstanceVO> NotMigratingSearch; protected SearchBuilder<VMInstanceVO> BackupSearch; protected SearchBuilder<VMInstanceVO> LastHostAndStatesSearch; + protected SearchBuilder<VMInstanceVO> VmsNotInClusterUsingPool; @Inject - ResourceTagDao _tagsDao; + ResourceTagDao tagsDao; @Inject - NicDao _nicDao; + NicDao nicDao; + @Inject + VolumeDao volumeDao; + @Inject + HostDao hostDao; protected Attribute _updateTimeAttr; @@ -278,7 +286,7 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem _updateTimeAttr = _allAttributes.get("updateTime"); assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; - SearchBuilder<NicVO> nicSearch = _nicDao.createSearchBuilder(); + SearchBuilder<NicVO> nicSearch = nicDao.createSearchBuilder(); nicSearch.and("networkId", nicSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); nicSearch.and("removedNic", nicSearch.entity().getRemoved(), SearchCriteria.Op.NULL); @@ -307,6 +315,16 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem LastHostAndStatesSearch.and("states", LastHostAndStatesSearch.entity().getState(), Op.IN); LastHostAndStatesSearch.done(); + VmsNotInClusterUsingPool = createSearchBuilder(); + SearchBuilder<VolumeVO> volumeSearch = volumeDao.createSearchBuilder(); + volumeSearch.and("poolId", volumeSearch.entity().getPoolId(), Op.EQ); + volumeSearch.and("removed", volumeSearch.entity().getRemoved(), Op.NULL); + VmsNotInClusterUsingPool.join("volumeSearch", volumeSearch, volumeSearch.entity().getInstanceId(), VmsNotInClusterUsingPool.entity().getId(), JoinType.INNER); + SearchBuilder<HostVO> hostSearch2 = hostDao.createSearchBuilder(); + hostSearch2.and("clusterId", hostSearch2.entity().getClusterId(), SearchCriteria.Op.NEQ); + VmsNotInClusterUsingPool.join("hostSearch2", hostSearch2, hostSearch2.entity().getId(), VmsNotInClusterUsingPool.entity().getHostId(), JoinType.INNER); + VmsNotInClusterUsingPool.and("vmStates", VmsNotInClusterUsingPool.entity().getState(), Op.IN); + VmsNotInClusterUsingPool.done(); } @Override @@ -836,7 +854,7 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem public List<VMInstanceVO> listNonRemovedVmsByTypeAndNetwork(long networkId, VirtualMachine.Type... types) { if (NetworkTypeSearch == null) { - SearchBuilder<NicVO> nicSearch = _nicDao.createSearchBuilder(); + SearchBuilder<NicVO> nicSearch = nicDao.createSearchBuilder(); nicSearch.and("networkId", nicSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); NetworkTypeSearch = createSearchBuilder(); @@ -873,7 +891,7 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem txn.start(); VMInstanceVO vm = findById(id); if (vm != null && vm.getType() == Type.User) { - _tagsDao.removeByIdAndType(id, ResourceObjectType.UserVm); + tagsDao.removeByIdAndType(id, ResourceObjectType.UserVm); } boolean result = super.remove(id); txn.commit(); @@ -1018,4 +1036,15 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem sc.setParameters("podId", String.valueOf(podId)); return listBy(sc); } + + @Override + public Pair<List<VMInstanceVO>, Integer> listByVmsNotInClusterUsingPool(long clusterId, long poolId) { + SearchCriteria<VMInstanceVO> sc = VmsNotInClusterUsingPool.create(); + sc.setParameters("vmStates", State.Starting, State.Running, State.Stopping, State.Migrating, State.Restoring); + sc.setJoinParameters("volumeSearch", "poolId", poolId); + sc.setJoinParameters("hostSearch2", "clusterId", clusterId); + List<VMInstanceVO> vms = search(sc, null); + List<VMInstanceVO> uniqueVms = vms.stream().distinct().collect(Collectors.toList()); + return new Pair<>(uniqueVms, uniqueVms.size()); + } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelper.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelper.java index fbb4a6e1618..216d6042f36 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelper.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelper.java @@ -28,31 +28,34 @@ import javax.inject.Inject; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; import com.cloud.agent.api.StoragePoolInfo; import com.cloud.capacity.Capacity; import com.cloud.capacity.CapacityVO; import com.cloud.capacity.dao.CapacityDao; -import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.StoragePoolStatus; -import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.utils.crypt.DBEncryptionUtil; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; @Component @@ -265,4 +268,48 @@ public class PrimaryDataStoreHelper { return true; } + public void switchToZone(DataStore store, HypervisorType hypervisorType) { + StoragePoolVO pool = dataStoreDao.findById(store.getId()); + CapacityVO capacity = _capacityDao.findByHostIdType(store.getId(), Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED); + Transaction.execute(new TransactionCallbackNoReturn() { + public void doInTransactionWithoutResult(TransactionStatus status) { + pool.setScope(ScopeType.ZONE); + pool.setPodId(null); + pool.setClusterId(null); + pool.setHypervisor(hypervisorType); + dataStoreDao.update(pool.getId(), pool); + + capacity.setPodId(null); + capacity.setClusterId(null); + _capacityDao.update(capacity.getId(), capacity); + } + }); + s_logger.debug("Scope of storage pool id=" + pool.getId() + " is changed to zone"); + } + + public void switchToCluster(DataStore store, ClusterScope clusterScope) { + List<StoragePoolHostVO> hostPoolRecords = storagePoolHostDao.listByPoolIdNotInCluster(clusterScope.getScopeId(), store.getId()).first(); + StoragePoolVO pool = dataStoreDao.findById(store.getId()); + CapacityVO capacity = _capacityDao.findByHostIdType(store.getId(), Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED); + + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + if (hostPoolRecords != null) { + for (StoragePoolHostVO host : hostPoolRecords) { + storagePoolHostDao.deleteStoragePoolHostDetails(host.getHostId(), host.getPoolId()); + } + } + pool.setScope(ScopeType.CLUSTER); + pool.setPodId(clusterScope.getPodId()); + pool.setClusterId(clusterScope.getScopeId()); + dataStoreDao.update(pool.getId(), pool); + + capacity.setPodId(clusterScope.getPodId()); + capacity.setClusterId(clusterScope.getScopeId()); + _capacityDao.update(capacity.getId(), capacity); + } + }); + s_logger.debug("Scope of storage pool id=" + pool.getId() + " is changed to cluster id=" + clusterScope.getScopeId()); + } } diff --git a/engine/storage/src/test/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelperTest.java b/engine/storage/src/test/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelperTest.java new file mode 100644 index 00000000000..3927b43f393 --- /dev/null +++ b/engine/storage/src/test/java/org/apache/cloudstack/storage/volume/datastore/PrimaryDataStoreHelperTest.java @@ -0,0 +1,114 @@ +// 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.volume.datastore; + +import java.util.List; + +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.capacity.Capacity; +import com.cloud.capacity.CapacityVO; +import com.cloud.capacity.dao.CapacityDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.utils.Pair; + +@RunWith(MockitoJUnitRunner.class) +public class PrimaryDataStoreHelperTest { + + @Mock + private PrimaryDataStoreDao dataStoreDao; + + @Mock + private CapacityDao capacityDao; + + @Mock + private StoragePoolHostDao storagePoolHostDao; + + @Spy + @InjectMocks + PrimaryDataStoreHelper dataStoreHelper; + + private static final Long ZONE_ID = 1L; + private static final Long CLUSTER_ID = 2L; + private static final Long POD_ID = 3L; + private static final Long POOL_ID = 4L; + private static final Short capacityType = 0; + private static final Float usedPercentage = 0.0f; + + @Test + public void testSwitchToZone() { + StoragePoolVO pool = new StoragePoolVO(POOL_ID, null, null, Storage.StoragePoolType.NetworkFilesystem, ZONE_ID, POD_ID, 0L, 0L, null, 0, null); + pool.setClusterId(CLUSTER_ID); + pool.setScope(ScopeType.CLUSTER); + CapacityVO capacity = new CapacityVO(ZONE_ID, POD_ID, CLUSTER_ID, capacityType, usedPercentage); + + Mockito.when(dataStoreDao.findById(pool.getId())).thenReturn(pool); + Mockito.when(capacityDao.findByHostIdType(pool.getId(), Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED)).thenReturn(capacity); + DataStore storeMock = Mockito.mock(DataStore.class); + Mockito.when(storeMock.getId()).thenReturn(POOL_ID); + + dataStoreHelper.switchToZone(storeMock, HypervisorType.KVM); + + Assert.assertEquals(pool.getScope(), ScopeType.ZONE); + Assert.assertEquals(pool.getPodId(), null); + Assert.assertEquals(pool.getClusterId(), null); + Assert.assertEquals(pool.getHypervisor(), HypervisorType.KVM); + Assert.assertEquals(capacity.getPodId(), null); + Assert.assertEquals(capacity.getClusterId(), null); + } + + @Test + public void testSwitchToCluster() { + StoragePoolVO pool = new StoragePoolVO(POOL_ID, null, null, Storage.StoragePoolType.NetworkFilesystem, ZONE_ID, null, 0L, 0L, null, 0, null); + pool.setScope(ScopeType.ZONE); + CapacityVO capacity = new CapacityVO(ZONE_ID, null, null, capacityType, usedPercentage); + ClusterScope clusterScope = new ClusterScope(CLUSTER_ID, POD_ID, ZONE_ID); + + Pair<List<StoragePoolHostVO>, Integer> hostPoolRecords = new Pair<>(null, 0); + Mockito.when(storagePoolHostDao.listByPoolIdNotInCluster(CLUSTER_ID, POOL_ID)).thenReturn(hostPoolRecords); + Mockito.when(dataStoreDao.findById(pool.getId())).thenReturn(pool); + Mockito.when(capacityDao.findByHostIdType(pool.getId(), Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED)).thenReturn(capacity); + DataStore storeMock = Mockito.mock(DataStore.class); + Mockito.when(storeMock.getId()).thenReturn(POOL_ID); + + dataStoreHelper.switchToCluster(storeMock, clusterScope); + + Mockito.verify(storagePoolHostDao, Mockito.never()).deleteStoragePoolHostDetails(Mockito.anyLong(), Mockito.anyLong()); + + Assert.assertEquals(pool.getScope(), ScopeType.CLUSTER); + Assert.assertEquals(pool.getPodId(), POD_ID); + Assert.assertEquals(pool.getClusterId(), CLUSTER_ID); + Assert.assertEquals(capacity.getPodId(), POD_ID); + Assert.assertEquals(capacity.getClusterId(), CLUSTER_ID); + } +} diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImpl.java new file mode 100644 index 00000000000..adc74a77d43 --- /dev/null +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImpl.java @@ -0,0 +1,106 @@ +// 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.lifecycle; + +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; +import org.apache.log4j.Logger; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.DeleteStoragePoolCommand; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.utils.Pair; + +public class BasePrimaryDataStoreLifeCycleImpl { + private static final Logger s_logger = Logger.getLogger(BasePrimaryDataStoreLifeCycleImpl.class); + @Inject + AgentManager agentMgr; + @Inject + protected ResourceManager resourceMgr; + @Inject + StorageManager storageMgr; + @Inject + PrimaryDataStoreHelper dataStoreHelper; + @Inject + protected HostDao hostDao; + @Inject + protected StoragePoolHostDao storagePoolHostDao; + + private List<HostVO> getPoolHostsList(ClusterScope clusterScope, HypervisorType hypervisorType) { + List<HostVO> hosts; + if (hypervisorType != null) { + hosts = resourceMgr + .listAllHostsInOneZoneNotInClusterByHypervisor(hypervisorType, clusterScope.getZoneId(), clusterScope.getScopeId()); + } else { + List<HypervisorType> hypervisorTypes = Arrays.asList(HypervisorType.KVM, HypervisorType.VMware); + hosts = resourceMgr + .listAllHostsInOneZoneNotInClusterByHypervisors(hypervisorTypes, clusterScope.getZoneId(), clusterScope.getScopeId()); + } + return hosts; + } + + public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + List<HostVO> hosts = getPoolHostsList(clusterScope, hypervisorType); + s_logger.debug("Changing scope of the storage pool to Zone"); + if (hosts != null) { + for (HostVO host : hosts) { + try { + storageMgr.connectHostToSharedPool(host.getId(), store.getId()); + } catch (Exception e) { + s_logger.warn("Unable to establish a connection between " + host + " and " + store, e); + } + } + } + dataStoreHelper.switchToZone(store, hypervisorType); + } + + public void changeStoragePoolScopeToCluster(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + Pair<List<StoragePoolHostVO>, Integer> hostPoolRecords = storagePoolHostDao.listByPoolIdNotInCluster(clusterScope.getScopeId(), store.getId()); + s_logger.debug("Changing scope of the storage pool to Cluster"); + if (hostPoolRecords.second() > 0) { + StoragePool pool = (StoragePool) store; + for (StoragePoolHostVO host : hostPoolRecords.first()) { + DeleteStoragePoolCommand deleteCmd = new DeleteStoragePoolCommand(pool); + final Answer answer = agentMgr.easySend(host.getHostId(), deleteCmd); + + if (answer != null) { + if (!answer.getResult()) { + s_logger.debug("Failed to delete storage pool: " + answer.getResult()); + } else if (HypervisorType.KVM != hypervisorType) { + break; + } + } + } + } + dataStoreHelper.switchToCluster(store, clusterScope); + } +} diff --git a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImplTest.java b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImplTest.java new file mode 100644 index 00000000000..355eb075129 --- /dev/null +++ b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/BasePrimaryDataStoreLifeCycleImplTest.java @@ -0,0 +1,127 @@ +// 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.lifecycle; + +import static org.mockito.ArgumentMatchers.eq; + +import java.util.Arrays; +import java.util.List; + +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.storage.datastore.PrimaryDataStoreImpl; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.DeleteStoragePoolCommand; +import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.Storage; +import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.utils.Pair; + +@RunWith(MockitoJUnitRunner.class) +public class BasePrimaryDataStoreLifeCycleImplTest { + + @Mock + private StoragePoolHostDao storagePoolHostDao; + + @Mock + private PrimaryDataStoreHelper dataStoreHelper; + + @Mock + private AgentManager agentManager; + + @Mock + private ResourceManager resourceManager; + + @Mock + private StorageManager storageManager; + + @Spy + @InjectMocks + private BasePrimaryDataStoreLifeCycleImpl dataStoreLifeCycle; + + private static final Long POOL_ID = 1L; + private static final Long CLUSTER_ID = 2L; + private static final Long POD_ID = 3L; + private static final Long ZONE_ID = 4L; + private static final Long HOST_ID = 5L; + + private static ClusterScope clusterScope; + private static PrimaryDataStoreImpl store; + + + @BeforeClass + public static void init() { + clusterScope = new ClusterScope(CLUSTER_ID, POD_ID, ZONE_ID); + StoragePoolVO pool = new StoragePoolVO(POOL_ID, null, null, Storage.StoragePoolType.NetworkFilesystem, 0L, 0L, 0L, 0L, null, 0, null); + store = new PrimaryDataStoreImpl(); + store.configure(pool, null, null); + } + + @Test + public void testChangeStoragePoolScopeToZone() throws Exception { + Mockito.when(resourceManager.listAllHostsInOneZoneNotInClusterByHypervisor(HypervisorType.KVM, ZONE_ID, CLUSTER_ID)).thenReturn(null); + + dataStoreLifeCycle.changeStoragePoolScopeToZone(store, clusterScope, HypervisorType.KVM); + + Mockito.verify(dataStoreHelper, Mockito.times(1)).switchToZone(store, HypervisorType.KVM); + + HostVO host = new HostVO(null); + ReflectionTestUtils.setField(host, "id", HOST_ID); + List<HypervisorType> hypervisorTypes = Arrays.asList(HypervisorType.KVM, HypervisorType.VMware); + Mockito.when(resourceManager.listAllHostsInOneZoneNotInClusterByHypervisors(hypervisorTypes, ZONE_ID, CLUSTER_ID)).thenReturn(Arrays.asList(host)); + Mockito.when(storageManager.connectHostToSharedPool(HOST_ID, POOL_ID)).thenReturn(true); + + dataStoreLifeCycle.changeStoragePoolScopeToZone(store, clusterScope, null); + + Mockito.verify(dataStoreHelper, Mockito.times(1)).switchToZone(store, null); + } + + @Test + public void testChangeStoragePoolScopeToCluster() { + Pair<List<StoragePoolHostVO>, Integer> hostPoolRecords = new Pair<>(null, 0); + Mockito.when(storagePoolHostDao.listByPoolIdNotInCluster(CLUSTER_ID, POOL_ID)).thenReturn(hostPoolRecords); + Mockito.doNothing().when(dataStoreHelper).switchToCluster(store, clusterScope); + + dataStoreLifeCycle.changeStoragePoolScopeToCluster(store, clusterScope, HypervisorType.KVM); + + hostPoolRecords.set(Arrays.asList(new StoragePoolHostVO(POOL_ID, HOST_ID, null)), 1); + Answer answer = new Answer(null, false, null); + Mockito.when(storagePoolHostDao.listByPoolIdNotInCluster(CLUSTER_ID, POOL_ID)).thenReturn(hostPoolRecords); + Mockito.when(agentManager.easySend(eq(HOST_ID), Mockito.any(DeleteStoragePoolCommand.class))).thenReturn(answer); + + dataStoreLifeCycle.changeStoragePoolScopeToCluster(store, clusterScope, HypervisorType.KVM); + + Mockito.verify(dataStoreHelper, Mockito.times(2)).switchToCluster(store, clusterScope); + } +} diff --git a/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java b/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java index ab1d49d8b4f..d1877bf09f0 100644 --- a/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java @@ -62,7 +62,7 @@ import com.cloud.host.Host; /** * Manages the lifecycle of a Managed Data Store in CloudStack */ -public class AdaptiveDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { +public class AdaptiveDataStoreLifeCycleImpl extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { @Inject private PrimaryDataStoreDao _storagePoolDao; private static final Logger s_logger = Logger.getLogger(AdaptiveDataStoreLifeCycleImpl.class); diff --git a/plugins/storage/volume/cloudbyte/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ElastistorPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/cloudbyte/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ElastistorPrimaryDataStoreLifeCycle.java index 0798f9f2cd2..9ad63faebf7 100644 --- a/plugins/storage/volume/cloudbyte/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ElastistorPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/cloudbyte/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/ElastistorPrimaryDataStoreLifeCycle.java @@ -65,7 +65,7 @@ import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.utils.exception.CloudRuntimeException; -public class ElastistorPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class ElastistorPrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger s_logger = Logger.getLogger(ElastistorPrimaryDataStoreLifeCycle.class); @Inject diff --git a/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/DateraPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/DateraPrimaryDataStoreLifeCycle.java index 6fd42009125..dae7d63d595 100644 --- a/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/DateraPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/DateraPrimaryDataStoreLifeCycle.java @@ -20,11 +20,11 @@ package org.apache.cloudstack.storage.datastore.lifecycle; import com.cloud.agent.api.StoragePoolInfo; import com.cloud.capacity.CapacityManager; +import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenterVO; -import com.cloud.dc.dao.DataCenterDao; -import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; @@ -43,10 +43,10 @@ import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.util.DateraUtil; @@ -58,7 +58,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -public class DateraPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class DateraPrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger s_logger = Logger.getLogger(DateraPrimaryDataStoreLifeCycle.class); @Inject @@ -395,6 +395,15 @@ public class DateraPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycl dataStoreHelper.disable(dataStore); } + @Override + public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + /* + * We need to attach all VMware, Xenserver and KVM hosts in the zone. + * So pass hypervisorType as null. + */ + super.changeStoragePoolScopeToZone(store, clusterScope, null); + } + private HypervisorType getHypervisorTypeForCluster(long clusterId) { ClusterVO cluster = _clusterDao.findById(clusterId); diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java index 73d9ca4b4f9..4d6fc09dc57 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java @@ -72,7 +72,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; -public class CloudStackPrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { +public class CloudStackPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger s_logger = Logger.getLogger(CloudStackPrimaryDataStoreLifeCycleImpl.class); @Inject protected ResourceManager _resourceMgr; diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java index efc69438e75..0ef97ae4796 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java @@ -53,7 +53,7 @@ import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; import org.apache.log4j.Logger; -public class LinstorPrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { +public class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger s_logger = Logger.getLogger(LinstorPrimaryDataStoreLifeCycleImpl.class); @Inject diff --git a/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java index 507189edc14..922803ce9db 100644 --- a/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; +import org.apache.cloudstack.storage.datastore.lifecycle.BasePrimaryDataStoreLifeCycleImpl; import org.apache.cloudstack.storage.datastore.util.NexentaUtil; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; import org.apache.log4j.Logger; @@ -45,6 +46,7 @@ import com.cloud.storage.StoragePool; import com.cloud.storage.StoragePoolAutomation; public class NexentaPrimaryDataStoreLifeCycle + extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger logger = Logger.getLogger(NexentaPrimaryDataStoreLifeCycle.class); @@ -177,6 +179,15 @@ public class NexentaPrimaryDataStoreLifeCycle dataStoreHelper.disable(dataStore); } + @Override + public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, Hypervisor.HypervisorType hypervisorType) { + /* + * We need to attach all VMware, Xenserver and KVM hosts in the zone. + * So pass hypervisorType as null. + */ + super.changeStoragePoolScopeToZone(store, clusterScope, null); + } + @Override public boolean deleteDataStore(DataStore store) { return dataStoreHelper.deletePrimaryDataStore(store); diff --git a/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SamplePrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SamplePrimaryDataStoreLifeCycleImpl.java index 3a0ce83f951..24d277251ea 100644 --- a/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SamplePrimaryDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SamplePrimaryDataStoreLifeCycleImpl.java @@ -146,4 +146,12 @@ public class SamplePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLife @Override public void disableStoragePool(DataStore store) { } + + @Override + public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + } + + @Override + public void changeStoragePoolScopeToCluster(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + } } 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 2d7aca11f84..c1a7411d29f 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 @@ -74,7 +74,7 @@ import com.cloud.utils.UriUtils; import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.exception.CloudRuntimeException; -public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class ScaleIOPrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger LOGGER = Logger.getLogger(ScaleIOPrimaryDataStoreLifeCycle.class); @Inject diff --git a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFirePrimaryDataStoreLifeCycle.java b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFirePrimaryDataStoreLifeCycle.java index 7a2767c32e6..0e6474de0bb 100644 --- a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFirePrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFirePrimaryDataStoreLifeCycle.java @@ -63,7 +63,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.google.common.base.Preconditions; -public class SolidFirePrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class SolidFirePrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger s_logger = Logger.getLogger(SolidFirePrimaryDataStoreLifeCycle.class); @Inject private CapacityManager _capacityMgr; @@ -387,4 +387,13 @@ public class SolidFirePrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeC public void disableStoragePool(DataStore dataStore) { _dataStoreHelper.disable(dataStore); } + + @Override + public void changeStoragePoolScopeToZone(DataStore store, ClusterScope clusterScope, HypervisorType hypervisorType) { + /* + * We need to attach all VMware, Xenserver and KVM hosts in the zone. + * So pass hypervisorType as null. + */ + super.changeStoragePoolScopeToZone(store, clusterScope, null); + } } diff --git a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFireSharedPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFireSharedPrimaryDataStoreLifeCycle.java index 557cc3f60f6..dc106d398e1 100644 --- a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFireSharedPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SolidFireSharedPrimaryDataStoreLifeCycle.java @@ -72,7 +72,7 @@ import com.cloud.user.dao.AccountDao; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.exception.CloudRuntimeException; -public class SolidFireSharedPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class SolidFireSharedPrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger LOGGER = Logger.getLogger(SolidFireSharedPrimaryDataStoreLifeCycle.class); @Inject private AccountDao accountDao; diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java index 4dbc7e4a22c..47f6089d734 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java @@ -60,7 +60,7 @@ import com.cloud.storage.dao.VMTemplateDetailsDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.utils.exception.CloudRuntimeException; -public class StorPoolPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { +public class StorPoolPrimaryDataStoreLifeCycle extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle { private static final Logger log = Logger.getLogger(StorPoolPrimaryDataStoreLifeCycle.class); @Inject 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 698c3d7fa33..5286dad29e4 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -100,6 +100,7 @@ import org.apache.cloudstack.api.command.admin.storage.ListStorageTagsCmd; import org.apache.cloudstack.api.command.admin.storage.heuristics.ListSecondaryStorageSelectorsCmd; import org.apache.cloudstack.api.command.admin.template.ListTemplatesCmdByAdmin; import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; +import org.apache.cloudstack.api.command.admin.vm.ListAffectedVmsForStorageScopeChangeCmd; import org.apache.cloudstack.api.command.admin.zone.ListZonesCmdByAdmin; import org.apache.cloudstack.api.command.user.account.ListAccountsCmd; import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; @@ -155,6 +156,7 @@ import org.apache.cloudstack.api.response.StorageTagResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.backup.BackupOfferingVO; @@ -243,8 +245,10 @@ import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.VolumeJoinVO; import com.cloud.cluster.ManagementServerHostVO; import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenter; import com.cloud.dc.DedicatedResourceVO; +import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DedicatedResourceDao; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; @@ -593,6 +597,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject private StoragePoolHostDao storagePoolHostDao; + @Inject + private ClusterDao clusterDao; + + private SearchCriteria<ServiceOfferingJoinVO> getMinimumCpuServiceOfferingJoinSearchCriteria(int cpu) { SearchCriteria<ServiceOfferingJoinVO> sc = _srvOfferingJoinDao.createSearchCriteria(); SearchCriteria<ServiceOfferingJoinVO> sc1 = _srvOfferingJoinDao.createSearchCriteria(); @@ -1147,6 +1155,58 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return response; } + @Override + public ListResponse<VirtualMachineResponse> listAffectedVmsForStorageScopeChange(ListAffectedVmsForStorageScopeChangeCmd cmd) { + Long poolId = cmd.getStorageId(); + StoragePoolVO pool = storagePoolDao.findById(poolId); + if (pool == null) { + throw new IllegalArgumentException("Unable to find storage pool with ID: " + poolId); + } + + ListResponse<VirtualMachineResponse> response = new ListResponse<>(); + List<VirtualMachineResponse> responsesList = new ArrayList<>(); + if (pool.getScope() != ScopeType.ZONE) { + response.setResponses(responsesList, 0); + return response; + } + + Pair<List<VMInstanceVO>, Integer> vms = _vmInstanceDao.listByVmsNotInClusterUsingPool(cmd.getClusterIdForScopeChange(), poolId); + for (VMInstanceVO vm : vms.first()) { + VirtualMachineResponse resp = new VirtualMachineResponse(); + resp.setObjectName(VirtualMachine.class.getSimpleName().toLowerCase()); + resp.setId(vm.getUuid()); + resp.setVmType(vm.getType().toString()); + + UserVmJoinVO userVM = null; + if (!vm.getType().isUsedBySystem()) { + userVM = _userVmJoinDao.findById(vm.getId()); + } + if (userVM != null) { + if (userVM.getDisplayName() != null) { + resp.setVmName(userVM.getDisplayName()); + } else { + resp.setVmName(userVM.getName()); + } + } else { + resp.setVmName(vm.getInstanceName()); + } + + HostVO host = hostDao.findById(vm.getHostId()); + if (host != null) { + resp.setHostId(host.getUuid()); + resp.setHostName(host.getName()); + ClusterVO cluster = clusterDao.findById(host.getClusterId()); + if (cluster != null) { + resp.setClusterId(cluster.getUuid()); + resp.setClusterName(cluster.getName()); + } + } + responsesList.add(resp); + } + response.setResponses(responsesList, vms.second()); + return response; + } + private Object getObjectPossibleMethodValue(Object obj, String methodName) { Object result = null; diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index b9163a188ea..5d3ec62c56d 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -3426,6 +3426,26 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, return _hostGpuGroupsDao.customSearch(sc, searchFilter); } + @Override + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisor(final HypervisorType type, final long dcId, final long clusterId) { + final QueryBuilder<HostVO> sc = QueryBuilder.create(HostVO.class); + sc.and(sc.entity().getHypervisorType(), Op.EQ, type); + sc.and(sc.entity().getDataCenterId(), Op.EQ, dcId); + sc.and(sc.entity().getClusterId(), Op.NEQ, clusterId); + sc.and(sc.entity().getStatus(), Op.EQ, Status.Up); + return sc.list(); + } + + @Override + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisors(List<HypervisorType> types, final long dcId, final long clusterId) { + final QueryBuilder<HostVO> sc = QueryBuilder.create(HostVO.class); + sc.and(sc.entity().getHypervisorType(), Op.IN, types); + sc.and(sc.entity().getDataCenterId(), Op.EQ, dcId); + sc.and(sc.entity().getClusterId(), Op.NEQ, clusterId); + sc.and(sc.entity().getStatus(), Op.EQ, Status.Up); + return sc.list(); + } + @Override public boolean isGPUDeviceAvailable(final long hostId, final String groupName, final String vgpuType) { if(!listAvailableGPUDevice(hostId, groupName, vgpuType).isEmpty()) { diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 14afcc71245..9d69a52cfb8 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -211,6 +211,7 @@ import org.apache.cloudstack.api.command.admin.storage.AddImageStoreCmd; import org.apache.cloudstack.api.command.admin.storage.AddImageStoreS3CMD; import org.apache.cloudstack.api.command.admin.storage.AddObjectStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.CancelPrimaryStorageMaintenanceCmd; +import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; import org.apache.cloudstack.api.command.admin.storage.CreateSecondaryStagingStoreCmd; import org.apache.cloudstack.api.command.admin.storage.CreateStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.DeleteImageStoreCmd; @@ -522,6 +523,7 @@ import org.apache.cloudstack.api.command.user.vm.AddIpToVmNicCmd; import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.admin.vm.ListAffectedVmsForStorageScopeChangeCmd; import org.apache.cloudstack.api.command.user.vm.GetVMPasswordCmd; import org.apache.cloudstack.api.command.user.vm.ListNicsCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; @@ -3481,6 +3483,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(UpgradeRouterCmd.class); cmdList.add(AddSwiftCmd.class); cmdList.add(CancelPrimaryStorageMaintenanceCmd.class); + cmdList.add(ChangeStoragePoolScopeCmd.class); cmdList.add(CreateStoragePoolCmd.class); cmdList.add(DeletePoolCmd.class); cmdList.add(ListSwiftsCmd.class); @@ -3917,6 +3920,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(CreateSecondaryStorageSelectorCmd.class); cmdList.add(UpdateSecondaryStorageSelectorCmd.class); cmdList.add(RemoveSecondaryStorageSelectorCmd.class); + cmdList.add(ListAffectedVmsForStorageScopeChangeCmd.class); // Out-of-band management APIs for admins diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index de3ec02dc7a..74b19b61fd4 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -54,6 +54,7 @@ import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.storage.CancelPrimaryStorageMaintenanceCmd; +import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; import org.apache.cloudstack.api.command.admin.storage.CreateSecondaryStagingStoreCmd; import org.apache.cloudstack.api.command.admin.storage.CreateStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.DeleteImageStoreCmd; @@ -257,6 +258,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.DiskProfile; import com.cloud.vm.UserVmManager; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.VMInstanceDao; import com.google.common.collect.Sets; @@ -412,6 +414,9 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C private final Map<String, HypervisorHostListener> hostListeners = new HashMap<String, HypervisorHostListener>(); + private final Set<HypervisorType> zoneWidePoolSupportedHypervisorTypes = Sets.newHashSet(HypervisorType.KVM, HypervisorType.VMware, + HypervisorType.Hyperv, HypervisorType.LXC, HypervisorType.Any, HypervisorType.Simulator); + private static final String NFS_MOUNT_OPTIONS_INCORRECT = "An incorrect mount option was specified"; public boolean share(VMInstanceVO vm, List<VolumeVO> vols, HostVO host, boolean cancelPreviousShare) throws StorageUnavailableException { @@ -967,9 +972,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C throw new InvalidParameterValueException("Missing parameter hypervisor. Hypervisor type is required to create zone wide primary storage."); } - Set<HypervisorType> supportedHypervisorTypes = Sets.newHashSet(HypervisorType.KVM, HypervisorType.VMware, - HypervisorType.Hyperv, HypervisorType.LXC, HypervisorType.Any, HypervisorType.Simulator); - if (!supportedHypervisorTypes.contains(hypervisorType)) { + if (!zoneWidePoolSupportedHypervisorTypes.contains(hypervisorType)) { throw new InvalidParameterValueException("Zone wide storage pool is not supported for hypervisor type " + hypervisor); } } else { @@ -1249,6 +1252,115 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C return (PrimaryDataStoreInfo)_dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); } + private void changeStoragePoolScopeToZone(StoragePoolVO primaryStorage) { + /* + * For cluster wide primary storage the hypervisor type might not be set. + * So, get it from the clusterVO. + */ + Long clusterId = primaryStorage.getClusterId(); + ClusterVO clusterVO = _clusterDao.findById(clusterId); + HypervisorType hypervisorType = clusterVO.getHypervisorType(); + if (!zoneWidePoolSupportedHypervisorTypes.contains(hypervisorType)) { + throw new InvalidParameterValueException("Primary storage scope change to Zone is not supported for hypervisor type " + hypervisorType); + } + + DataStoreProvider storeProvider = _dataStoreProviderMgr.getDataStoreProvider(primaryStorage.getStorageProviderName()); + PrimaryDataStoreLifeCycle lifeCycle = (PrimaryDataStoreLifeCycle) storeProvider.getDataStoreLifeCycle(); + + DataStore primaryStore = _dataStoreMgr.getPrimaryDataStore(primaryStorage.getId()); + ClusterScope clusterScope = new ClusterScope(primaryStorage.getClusterId(), null, primaryStorage.getDataCenterId()); + + lifeCycle.changeStoragePoolScopeToZone(primaryStore, clusterScope, hypervisorType); + } + + private void changeStoragePoolScopeToCluster(StoragePoolVO primaryStorage, Long clusterId) { + if (clusterId == null) { + throw new InvalidParameterValueException("Cluster ID not provided"); + } + ClusterVO clusterVO = _clusterDao.findById(clusterId); + if (clusterVO == null) { + throw new InvalidParameterValueException("Unable to find cluster by id " + clusterId); + } + if (clusterVO.getAllocationState().equals(Grouping.AllocationState.Disabled)) { + throw new PermissionDeniedException("Cannot perform this operation, Cluster is currently disabled: " + clusterId); + } + + List<VirtualMachine.State> states = Arrays.asList(State.Starting, State.Running, State.Stopping, State.Migrating, State.Restoring); + + Long id = primaryStorage.getId(); + Pair<List<VMInstanceVO>, Integer> vmsNotInClusterUsingPool = _vmInstanceDao.listByVmsNotInClusterUsingPool(clusterId, id); + if (vmsNotInClusterUsingPool.second() != 0) { + throw new CloudRuntimeException(String.format("Cannot change scope of the storage pool [%s] to cluster [%s] " + + "as there are %s VMs with volumes in this pool that are running on other clusters. " + + "All such User VMs must be stopped and System VMs must be destroyed before proceeding. " + + "Please use the API listAffectedVmsForStorageScopeChange to get the list.", + primaryStorage.getName(), clusterVO.getName(), vmsNotInClusterUsingPool.second())); + } + + DataStoreProvider storeProvider = _dataStoreProviderMgr.getDataStoreProvider(primaryStorage.getStorageProviderName()); + PrimaryDataStoreLifeCycle lifeCycle = (PrimaryDataStoreLifeCycle) storeProvider.getDataStoreLifeCycle(); + + DataStore primaryStore = _dataStoreMgr.getPrimaryDataStore(id); + ClusterScope clusterScope = new ClusterScope(clusterId, clusterVO.getPodId(), primaryStorage.getDataCenterId()); + + lifeCycle.changeStoragePoolScopeToCluster(primaryStore, clusterScope, primaryStorage.getHypervisor()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CHANGE_STORAGE_POOL_SCOPE, eventDescription = "changing storage pool scope") + public void changeStoragePoolScope(ChangeStoragePoolScopeCmd cmd) throws IllegalArgumentException, InvalidParameterValueException, PermissionDeniedException { + Long id = cmd.getId(); + + Long accountId = cmd.getEntityOwnerId(); + if (!_accountMgr.isRootAdmin(accountId)) { + throw new PermissionDeniedException("Only root admin can perform this operation"); + } + + ScopeType newScope = EnumUtils.getEnumIgnoreCase(ScopeType.class, cmd.getScope()); + if (newScope != ScopeType.ZONE && newScope != ScopeType.CLUSTER) { + throw new InvalidParameterValueException("Invalid scope " + cmd.getScope() + "for Primary storage"); + } + + StoragePoolVO primaryStorage = _storagePoolDao.findById(id); + if (primaryStorage == null) { + throw new IllegalArgumentException("Unable to find storage pool with ID: " + id); + } + + String eventDetails = String.format(" Storage pool Id: %s to %s",primaryStorage.getUuid(), newScope); + CallContext.current().setEventDetails(eventDetails); + + ScopeType currentScope = primaryStorage.getScope(); + if (currentScope.equals(newScope)) { + throw new InvalidParameterValueException("New scope must be different than the current scope"); + } + + if (currentScope != ScopeType.ZONE && currentScope != ScopeType.CLUSTER) { + throw new InvalidParameterValueException("This operation is supported only for Primary storages having scope " + + ScopeType.CLUSTER + " or " + ScopeType.ZONE); + } + + if (!primaryStorage.getStatus().equals(StoragePoolStatus.Disabled)) { + throw new InvalidParameterValueException("Scope of the Primary storage with id " + + primaryStorage.getUuid() + + " cannot be changed, as it is not in the Disabled state"); + } + + Long zoneId = primaryStorage.getDataCenterId(); + DataCenterVO zone = _dcDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Unable to find zone by id " + zoneId); + } + if (zone.getAllocationState().equals(Grouping.AllocationState.Disabled)) { + throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zoneId); + } + + if (newScope.equals(ScopeType.ZONE)) { + changeStoragePoolScopeToZone(primaryStorage); + } else { + changeStoragePoolScopeToCluster(primaryStorage, cmd.getClusterId()); + } + } + @Override public void removeStoragePoolFromCluster(long hostId, String iScsiName, StoragePool storagePool) { final Map<String, String> details = new HashMap<>(); diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index be8978e799b..2a62adef06b 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -18,18 +18,25 @@ package com.cloud.api.query; import com.cloud.api.query.dao.TemplateJoinDao; +import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.TemplateJoinVO; +import com.cloud.api.query.vo.UserVmJoinVO; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; import com.cloud.event.EventVO; import com.cloud.event.dao.EventDao; import com.cloud.event.dao.EventJoinDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.network.Network; import com.cloud.network.VNF; import com.cloud.network.dao.NetworkVO; import com.cloud.server.ResourceTag; import com.cloud.storage.BucketVO; +import com.cloud.storage.ScopeType; import com.cloud.storage.dao.BucketDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@ -41,10 +48,14 @@ import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; + import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.command.admin.storage.ListObjectStoragePoolsCmd; +import org.apache.cloudstack.api.command.admin.vm.ListAffectedVmsForStorageScopeChangeCmd; import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; @@ -52,9 +63,12 @@ import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ObjectStoreResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao; import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -65,6 +79,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; import java.util.Arrays; @@ -112,9 +127,24 @@ public class QueryManagerImplTest { @Mock ObjectStoreDao objectStoreDao; + @Mock + VMInstanceDao vmInstanceDao; + + @Mock + PrimaryDataStoreDao storagePoolDao; + + @Mock + HostDao hostDao; + + @Mock + ClusterDao clusterDao; + @Mock BucketDao bucketDao; + @Mock + UserVmJoinDao userVmJoinDao; + private AccountVO account; private UserVO user; @@ -352,4 +382,46 @@ public class QueryManagerImplTest { when(bucketDao.searchAndCount(any(), any())).thenReturn(new Pair<>(buckets, 2)); queryManagerImplSpy.searchForBuckets(listBucketsCmd); } + + @Test + public void testListAffectedVmsForScopeChange() { + Long clusterId = 1L; + Long poolId = 2L; + Long hostId = 3L; + Long vmId = 4L; + String vmName = "VM1"; + + ListAffectedVmsForStorageScopeChangeCmd cmd = new ListAffectedVmsForStorageScopeChangeCmd(); + ReflectionTestUtils.setField(cmd, "clusterIdForScopeChange", clusterId); + ReflectionTestUtils.setField(cmd, "storageId", poolId); + + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool.getScope()).thenReturn(ScopeType.CLUSTER); + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(pool); + ListResponse<VirtualMachineResponse> response = queryManager.listAffectedVmsForStorageScopeChange(cmd); + Assert.assertEquals(response.getResponses().size(), 0); + + VMInstanceVO instance = Mockito.mock(VMInstanceVO.class); + UserVmJoinVO userVM = Mockito.mock(UserVmJoinVO.class); + String instanceUuid = String.valueOf(UUID.randomUUID()); + Pair<List<VMInstanceVO>, Integer> vms = new Pair<>(List.of(instance), 1); + HostVO host = Mockito.mock(HostVO.class); + ClusterVO cluster = Mockito.mock(ClusterVO.class); + + Mockito.when(pool.getScope()).thenReturn(ScopeType.ZONE); + Mockito.when(instance.getUuid()).thenReturn(instanceUuid); + Mockito.when(instance.getType()).thenReturn(VirtualMachine.Type.Instance); + Mockito.when(instance.getHostId()).thenReturn(hostId); + Mockito.when(instance.getId()).thenReturn(vmId); + Mockito.when(userVM.getDisplayName()).thenReturn(vmName); + Mockito.when(vmInstanceDao.listByVmsNotInClusterUsingPool(clusterId, poolId)).thenReturn(vms); + Mockito.when(userVmJoinDao.findById(vmId)).thenReturn(userVM); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + Mockito.when(host.getClusterId()).thenReturn(clusterId); + Mockito.when(clusterDao.findById(clusterId)).thenReturn(cluster); + + response = queryManager.listAffectedVmsForStorageScopeChange(cmd); + Assert.assertEquals(response.getResponses().get(0).getId(), instanceUuid); + Assert.assertEquals(response.getResponses().get(0).getName(), vmName); + } } diff --git a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java index f9c07ef8dd8..6aae7a091d3 100755 --- a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java +++ b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java @@ -431,6 +431,17 @@ public class MockResourceManagerImpl extends ManagerBase implements ResourceMana return null; } + @Override + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisor(HypervisorType type, long dcId, long clusterId) { + return null; + } + + @Override + public List<HostVO> listAllHostsInOneZoneNotInClusterByHypervisors(List<HypervisorType> types, long dcId, long clusterId) { + // TODO Auto-generated method stub + return null; + } + /* (non-Javadoc) * @see com.cloud.resource.ResourceManager#listAvailHypervisorInZone(java.lang.Long, java.lang.Long) */ diff --git a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java index 2596f9facba..901676542c9 100644 --- a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java @@ -16,17 +16,22 @@ // under the License. package com.cloud.storage; +import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; import com.cloud.exception.ConnectionException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.host.Host; -import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.dao.VolumeDao; -import com.cloud.user.AccountManager; +import com.cloud.user.AccountManagerImpl; import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; @@ -46,6 +51,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; import java.util.HashMap; @@ -67,10 +73,16 @@ public class StorageManagerImplTest { @Mock DataCenterDao dataCenterDao; @Mock - AccountManager accountManager; + AccountManagerImpl accountMgr; @Mock StoragePoolDetailsDao storagePoolDetailsDao; + @Mock + ClusterDao clusterDao; + + @Mock + PrimaryDataStoreDao storagePoolDao; + @Spy @InjectMocks private StorageManagerImpl storageManagerImpl; @@ -260,11 +272,74 @@ public class StorageManagerImplTest { .update(StorageManager.DataStoreDownloadFollowRedirects.key(),StorageManager.DataStoreDownloadFollowRedirects.defaultValue()); } + private ChangeStoragePoolScopeCmd mockChangeStoragePooolScopeCmd(String newScope) { + ChangeStoragePoolScopeCmd cmd = new ChangeStoragePoolScopeCmd(); + ReflectionTestUtils.setField(cmd, "id", 1L); + ReflectionTestUtils.setField(cmd, "clusterId", 1L); + ReflectionTestUtils.setField(cmd, "scope", newScope); + return cmd; + } + + private StoragePoolVO mockStoragePoolVOForChangeStoragePoolScope(ScopeType currentScope, StoragePoolStatus status) { + StoragePoolVO primaryStorage = new StoragePoolVO(); + primaryStorage.setId(1L); + primaryStorage.setDataCenterId(1L); + primaryStorage.setClusterId(1L); + primaryStorage.setStatus(StoragePoolStatus.Disabled); + primaryStorage.setScope(currentScope); + primaryStorage.setStatus(status); + return primaryStorage; + } + + private void prepareTestChangeStoragePoolScope(ScopeType currentScope, StoragePoolStatus status) { + final DataCenterVO zone = new DataCenterVO(1L, null, null, null, null, null, null, null, null, null, DataCenter.NetworkType.Advanced, null, null); + StoragePoolVO primaryStorage = mockStoragePoolVOForChangeStoragePoolScope(currentScope, status); + + Mockito.when(accountMgr.isRootAdmin(Mockito.any())).thenReturn(true); + Mockito.when(dataCenterDao.findById(1L)).thenReturn(zone); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(primaryStorage); + } + + @Test(expected = InvalidParameterValueException.class) + public void testChangeStoragePoolScopeNotDisabledException() { + prepareTestChangeStoragePoolScope(ScopeType.CLUSTER, StoragePoolStatus.Initialized); + + ChangeStoragePoolScopeCmd cmd = mockChangeStoragePooolScopeCmd("ZONE"); + storageManagerImpl.changeStoragePoolScope(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testChangeStoragePoolScopeToZoneHypervisorNotSupported() { + prepareTestChangeStoragePoolScope(ScopeType.CLUSTER, StoragePoolStatus.Disabled); + + final ClusterVO cluster = new ClusterVO(); + cluster.setHypervisorType(String.valueOf(HypervisorType.XenServer)); + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + + ChangeStoragePoolScopeCmd cmd = mockChangeStoragePooolScopeCmd("ZONE"); + storageManagerImpl.changeStoragePoolScope(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testChangeStoragePoolScopeToClusterVolumesPresentException() { + prepareTestChangeStoragePoolScope(ScopeType.ZONE, StoragePoolStatus.Disabled); + + final ClusterVO cluster = new ClusterVO(); + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + + VMInstanceVO instance = Mockito.mock(VMInstanceVO.class); + Pair<List<VMInstanceVO>, Integer> vms = new Pair<>(List.of(instance), 1); + Mockito.when(vmInstanceDao.listByVmsNotInClusterUsingPool(1L, 1L)).thenReturn(vms); + + ChangeStoragePoolScopeCmd cmd = mockChangeStoragePooolScopeCmd("CLUSTER"); + storageManagerImpl.changeStoragePoolScope(cmd); + } + @Test public void testCheckNFSMountOptionsForCreateNoNFSMountOptions() { Map<String, String> details = new HashMap<>(); try { - storageManagerImpl.checkNFSMountOptionsForCreate(details, Hypervisor.HypervisorType.XenServer, ""); + storageManagerImpl.checkNFSMountOptionsForCreate(details, HypervisorType.XenServer, ""); } catch (Exception e) { Assert.fail(); } @@ -275,8 +350,8 @@ public class StorageManagerImplTest { Map<String, String> details = new HashMap<>(); details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, - () -> storageManagerImpl.checkNFSMountOptionsForCreate(details, Hypervisor.HypervisorType.XenServer, "")); - Assert.assertEquals(exception.getMessage(), "NFS options can not be set for the hypervisor type " + Hypervisor.HypervisorType.XenServer); + () -> storageManagerImpl.checkNFSMountOptionsForCreate(details, HypervisorType.XenServer, "")); + Assert.assertEquals(exception.getMessage(), "NFS options can not be set for the hypervisor type " + HypervisorType.XenServer); } @Test @@ -284,7 +359,7 @@ public class StorageManagerImplTest { Map<String, String> details = new HashMap<>(); details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, - () -> storageManagerImpl.checkNFSMountOptionsForCreate(details, Hypervisor.HypervisorType.KVM, "")); + () -> storageManagerImpl.checkNFSMountOptionsForCreate(details, HypervisorType.KVM, "")); Assert.assertEquals(exception.getMessage(), "NFS options can only be set on pool type " + Storage.StoragePoolType.NetworkFilesystem); } @@ -306,7 +381,7 @@ public class StorageManagerImplTest { StoragePoolVO pool = new StoragePoolVO(); Long accountId = 1L; details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); - Mockito.when(accountManager.isRootAdmin(accountId)).thenReturn(false); + Mockito.when(accountMgr.isRootAdmin(accountId)).thenReturn(false); PermissionDeniedException exception = Assert.assertThrows(PermissionDeniedException.class, () -> storageManagerImpl.checkNFSMountOptionsForUpdate(details, pool, accountId)); Assert.assertEquals(exception.getMessage(), "Only root admin can modify nfs options"); @@ -318,11 +393,11 @@ public class StorageManagerImplTest { StoragePoolVO pool = new StoragePoolVO(); Long accountId = 1L; details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); - Mockito.when(accountManager.isRootAdmin(accountId)).thenReturn(true); - pool.setHypervisor(Hypervisor.HypervisorType.XenServer); + Mockito.when(accountMgr.isRootAdmin(accountId)).thenReturn(true); + pool.setHypervisor(HypervisorType.XenServer); InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, () -> storageManagerImpl.checkNFSMountOptionsForUpdate(details, pool, accountId)); - Assert.assertEquals(exception.getMessage(), "NFS options can only be set for the hypervisor type " + Hypervisor.HypervisorType.KVM); + Assert.assertEquals(exception.getMessage(), "NFS options can only be set for the hypervisor type " + HypervisorType.KVM); } @Test @@ -331,8 +406,8 @@ public class StorageManagerImplTest { StoragePoolVO pool = new StoragePoolVO(); Long accountId = 1L; details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); - Mockito.when(accountManager.isRootAdmin(accountId)).thenReturn(true); - pool.setHypervisor(Hypervisor.HypervisorType.KVM); + Mockito.when(accountMgr.isRootAdmin(accountId)).thenReturn(true); + pool.setHypervisor(HypervisorType.KVM); pool.setPoolType(Storage.StoragePoolType.FiberChannel); InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, () -> storageManagerImpl.checkNFSMountOptionsForUpdate(details, pool, accountId)); @@ -345,8 +420,8 @@ public class StorageManagerImplTest { StoragePoolVO pool = new StoragePoolVO(); Long accountId = 1L; details.put(ApiConstants.NFS_MOUNT_OPTIONS, "vers=4.1"); - Mockito.when(accountManager.isRootAdmin(accountId)).thenReturn(true); - pool.setHypervisor(Hypervisor.HypervisorType.KVM); + Mockito.when(accountMgr.isRootAdmin(accountId)).thenReturn(true); + pool.setHypervisor(HypervisorType.KVM); pool.setPoolType(Storage.StoragePoolType.NetworkFilesystem); pool.setStatus(StoragePoolStatus.Up); InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, @@ -359,7 +434,7 @@ public class StorageManagerImplTest { String nfsMountOpts = "vers=4.1, nconnect=4,vers=4.2"; Map<String, String> details = new HashMap<>(); details.put(ApiConstants.NFS_MOUNT_OPTIONS, nfsMountOpts); - storageManagerImpl.checkNFSMountOptionsForCreate(details, Hypervisor.HypervisorType.KVM, "nfs"); + storageManagerImpl.checkNFSMountOptionsForCreate(details, HypervisorType.KVM, "nfs"); } @Test(expected = InvalidParameterValueException.class) @@ -368,11 +443,11 @@ public class StorageManagerImplTest { Map<String, String> details = new HashMap<>(); details.put(ApiConstants.NFS_MOUNT_OPTIONS, nfsMountOpts); StoragePoolVO pool = new StoragePoolVO(); - pool.setHypervisor(Hypervisor.HypervisorType.KVM); + pool.setHypervisor(HypervisorType.KVM); pool.setPoolType(Storage.StoragePoolType.NetworkFilesystem); pool.setStatus(StoragePoolStatus.Maintenance); Long accountId = 1L; - Mockito.when(accountManager.isRootAdmin(accountId)).thenReturn(true); + Mockito.when(accountMgr.isRootAdmin(accountId)).thenReturn(true); storageManagerImpl.checkNFSMountOptionsForUpdate(details, pool, accountId); } diff --git a/test/integration/smoke/test_primary_storage_scope.py b/test/integration/smoke/test_primary_storage_scope.py new file mode 100644 index 00000000000..e85a06b99f0 --- /dev/null +++ b/test/integration/smoke/test_primary_storage_scope.py @@ -0,0 +1,176 @@ +# 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. +""" BVT tests for Primary Storage +""" + +# Import System modules +# Import Local Modules +from marvin.cloudstackTestCase import * +from marvin.lib.base import (Host, StoragePool, Cluster, updateStoragePool, changeStoragePoolScope) +from marvin.lib.common import (get_zone, get_pod, list_clusters) +from marvin.lib.utils import cleanup_resources +from nose.plugins.attrib import attr + +class TestPrimaryStorageScope(cloudstackTestCase): + + def setUp(self): + + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.services = self.testClient.getParsedTestDataConfig() + self._cleanup = [] + self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests()) + self.pod = get_pod(self.apiclient, self.zone.id) + self.debug("here") + self.debug(self.services) + self.cluster1 = list_clusters(self.apiclient)[0] + self.debug("here1") + self.debug(self.cluster1) + self.cluster = { + 'clustername': 'C0_testScope', + 'clustertype': 'CloudManaged' + } + self.cluster2 = Cluster.create(self.apiclient, + self.cluster, + zoneid=self.zone.id, + podid=self.pod.id, + hypervisor=self.cluster1.hypervisortype + ) + self._cleanup.append(self.cluster2) + self.storage = StoragePool.create(self.apiclient, + self.services["nfs"], + scope = 'ZONE', + zoneid=self.zone.id, + hypervisor=self.cluster1.hypervisortype + ) + self._cleanup.append(self.storage) + self.debug("Created storage pool %s in zone scope", self.storage.id) + return + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + @attr(tags=["advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") + def test_01_primary_storage_scope_change(self): + """Test primary storage pool scope change + """ + + # Disable storage pool + cmd = updateStoragePool.updateStoragePoolCmd() + cmd.id = self.storage.id + cmd.enabled = False + self.apiclient.updateStoragePool(cmd) + + self.debug("Disabled storage pool : %s" % self.storage.id) + + # Change storage pool scope to Cluster2 + cmd = changeStoragePoolScope.changeStoragePoolScopeCmd() + cmd.id = self.storage.id + cmd.scope = "CLUSTER" + cmd.clusterid = self.cluster2.id + self.apiclient.changeStoragePoolScope(cmd) + + self.debug("Changed scope of storage pool %s to cluster" % self.storage.id) + + pool_id = self.dbclient.execute("select id from storage_pool where uuid=\"" + self.storage.id + "\"")[0][0] + host1 = Host.list(self.apiclient, clusterid=self.cluster1.id, listall=True)[0] + host1_id = self.dbclient.execute("select id from host where uuid=\"" + host1.id + "\"")[0][0] + + pool_row = self.dbclient.execute("select cluster_id, pod_id, scope from storage_pool where id=" + str(pool_id))[0] + capacity_row = self.dbclient.execute("select cluster_id, pod_id from op_host_capacity where capacity_type=3 and host_id=" + str(pool_id))[0] + pool_host_rows = self.dbclient.execute("select id from storage_pool_host_ref where host_id=" + str(host1_id) + " and pool_id=" + str(pool_id)) + + self.assertIsNotNone( + pool_row[0], + "Cluster id should not be NULL for cluster scope" + ) + self.assertIsNotNone( + pool_row[1], + "Pod id should not be NULL for cluster scope" + ) + self.assertEqual( + pool_row[2], + "CLUSTER", + "Storage pool scope not changed to Cluster" + ) + self.assertIsNotNone( + capacity_row[0], + "Cluster id should not be NULL in the op_host_capacity table" + ) + self.assertIsNotNone( + capacity_row[1], + "Pod id set should not be NULL in the op_host_capacity table" + ) + self.assertEqual( + len(pool_host_rows), + 0, + "Storage pool not removed from the storage_pool_host_ref table for host on another cluster" + ) + + # Change storage pool scope to Zone + cmd = changeStoragePoolScope.changeStoragePoolScopeCmd() + cmd.id = self.storage.id + cmd.scope = "ZONE" + self.apiclient.changeStoragePoolScope(cmd) + + self.debug("Changed scope of storage pool %s to zone" % self.storage.id) + + pool_row = self.dbclient.execute("select cluster_id, pod_id, scope from storage_pool where id=" + str(pool_id))[0] + capacity_row = self.dbclient.execute("select cluster_id, pod_id from op_host_capacity where capacity_type=3 and host_id=" + str(pool_id))[0] + pool_host_rows = self.dbclient.execute("select id from storage_pool_host_ref where host_id=" + str(host1_id) + " and pool_id=" + str(pool_id)) + + self.assertIsNone( + pool_row[0], + "Cluster id not set to NULL for zone scope" + ) + self.assertIsNone( + pool_row[1], + "Pod id not set to NULL for zone scope" + ) + self.assertEqual( + pool_row[2], + "ZONE", + "Storage pool scope not changed to ZONE" + ) + self.assertIsNone( + capacity_row[0], + "Cluster id not set to NULL in the op_host_capacity table" + ) + self.assertIsNone( + capacity_row[1], + "Pod id not set to NULL in the op_host_capacity table" + ) + self.assertEqual( + len(pool_host_rows), + 1, + "Storage pool not added to the storage_pool_host_ref table for host on another cluster" + ) + + # Enable storage pool + cmd = updateStoragePool.updateStoragePoolCmd() + cmd.id = self.storage.id + cmd.enabled = True + response = self.apiclient.updateStoragePool(cmd) + self.assertEqual( + response.state, + "Up", + "Storage pool couldn't be enabled" + ) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index b971d244941..fc4d9dab514 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -142,6 +142,7 @@ known_categories = { 'StorageMaintenance': 'Storage Pool', 'StoragePool': 'Storage Pool', 'StorageProvider': 'Storage Pool', + 'StorageScope' : 'Storage Pool', 'updateStorageCapabilities' : 'Storage Pool', 'SecurityGroup': 'Security Group', 'SSH': 'SSH', diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 66d45056c96..c5d87d208b4 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -60,6 +60,7 @@ "label.action.bulk.release.public.ip.address": "Bulk release public IP addresses", "label.action.cancel.maintenance.mode": "Cancel maintenance mode", "label.action.change.password": "Change password", +"label.action.change.primary.storage.scope": "Change primary storage scope", "label.action.configure.stickiness": "Stickiness", "label.action.copy.iso": "Copy ISO", "label.action.copy.snapshot": "Copy Snapshot", @@ -2483,6 +2484,8 @@ "message.action.manage.cluster": "Please confirm that you want to manage the cluster.", "message.action.patch.router": "Please confirm that you want to live patch the router. <br> This operation is equivalent updating the router packages and restarting the Network without cleanup.", "message.action.patch.systemvm": "Please confirm that you want to patch the System VM.", +"message.action.primary.storage.scope.cluster": "Please confirm that you want to change the scope from zone to the specified cluster.<br>This operation will update the database and disconnect the storage pool from all hosts that were previously connected to the primary storage and are not part of the specified cluster.", +"message.action.primary.storage.scope.zone": "Please confirm that you want to change the scope from cluster to zone.<br>This operation will update the database and connect the storage pool to all hosts of the zone running the same hypervisor as set on the storage pool.", "message.action.primarystorage.enable.maintenance.mode": "Warning: placing the primary storage into maintenance mode will cause all Instances using volumes from it to be stopped. Do you want to continue?", "message.action.reboot.instance": "Please confirm that you want to reboot this Instance.", "message.action.reboot.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", @@ -2600,6 +2603,8 @@ "message.change.offering.for.volume.failed": "Change offering for the volume failed", "message.change.offering.for.volume.processing": "Changing offering for the volume...", "message.change.password": "Please change your password.", +"message.change.scope.failed": "Scope change failed", +"message.change.scope.processing": "Scope change in progress", "message.cluster.dedicated": "Cluster Dedicated", "message.cluster.dedication.released": "Cluster dedication released.", "message.config.health.monitor.failed": "Configure Health Monitor failed", @@ -3188,6 +3193,7 @@ "message.success.change.affinity.group": "Successfully changed affinity groups", "message.success.change.offering": "Successfully changed offering", "message.success.change.password": "Successfully changed password for User", +"message.success.change.scope": "Successfully changed scope for storage pool", "message.success.config.backup.schedule": "Successfully configured Instance backup schedule", "message.success.config.health.monitor": "Successfully Configure Health Monitor", "message.success.config.sticky.policy": "Successfully configured sticky policy", @@ -3374,6 +3380,7 @@ "message.volumes.unmanaged": "Volumes not controlled by CloudStack.", "message.vr.alert.upon.network.offering.creation.l2": "As virtual routers are not created for L2 Networks, the compute offering will not be used.", "message.vr.alert.upon.network.offering.creation.others": "As none of the obligatory services for creating a virtual router (VPN, DHCP, DNS, Firewall, LB, UserData, SourceNat, StaticNat, PortForwarding) are enabled, the virtual router will not be created and the compute offering will not be used.", +"message.warn.change.primary.storage.scope": "This feature is tested and supported for the following configurations:<br>KVM - NFS/Ceph - DefaultPrimary<br>VMware - NFS - DefaultPrimary<br>*There might be extra steps involved to make it work for other configurations.", "message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats.", "message.warn.importing.instance.without.nic": "WARNING: This Instance is being imported without NICs and many Network resources will not be available. Consider creating a NIC via vCenter before importing or as soon as the Instance is imported.", "message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network’s MTU settings", diff --git a/ui/src/config/section/infra/primaryStorages.js b/ui/src/config/section/infra/primaryStorages.js index 24333367f1c..c4932b2daad 100644 --- a/ui/src/config/section/infra/primaryStorages.js +++ b/ui/src/config/section/infra/primaryStorages.js @@ -135,6 +135,26 @@ export default { dataView: true, show: (record) => { return ['Maintenance', 'PrepareForMaintenance', 'ErrorInMaintenance'].includes(record.state) } }, + { + api: 'changeStoragePoolScope', + icon: 'swap-outlined', + label: 'label.action.change.primary.storage.scope', + dataView: true, + popup: true, + show: (record) => { + return (record.state === 'Disabled' && + (record.scope === 'CLUSTER' || + record.scope === 'ZONE') && + (record.hypervisor === 'KVM' || + record.hypervisor === 'VMware' || + record.hypervisor === 'HyperV' || + record.hypervisor === 'LXC' || + record.hypervisor === 'Any' || + record.hypervisor === 'Simulator') + ) + }, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ChangeStoragePoolScope.vue'))) + }, { api: 'deleteStoragePool', icon: 'delete-outlined', diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index 2e0eea4f84b..ce57625d6e7 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -21,6 +21,7 @@ import { ApiOutlined, AppstoreOutlined, ArrowDownOutlined, + ArrowRightOutlined, ArrowUpOutlined, ArrowsAltOutlined, AuditOutlined, @@ -182,6 +183,7 @@ export default { app.component('ApiOutlined', ApiOutlined) app.component('AppstoreOutlined', AppstoreOutlined) app.component('ArrowDownOutlined', ArrowDownOutlined) + app.component('ArrowRightOutlined', ArrowRightOutlined) app.component('ArrowUpOutlined', ArrowUpOutlined) app.component('ArrowsAltOutlined', ArrowsAltOutlined) app.component('AuditOutlined', AuditOutlined) diff --git a/ui/src/views/infra/ChangeStoragePoolScope.vue b/ui/src/views/infra/ChangeStoragePoolScope.vue new file mode 100644 index 00000000000..1e1c14201b9 --- /dev/null +++ b/ui/src/views/infra/ChangeStoragePoolScope.vue @@ -0,0 +1,223 @@ +// 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. + +<template> + <a-spin :spinning="loading"> + <div class="form-layout" v-ctrl-enter="handleSubmitForm"> + <div class="form"> + <a-form + :ref="formRef" + :model="form" + :rules="rules" + layout="vertical" + @submit="handleSubmitForm"> + <a-alert type="warning"> + <template #message> + <span + v-html="(resource.scope=='ZONE' ? $t('message.action.primary.storage.scope.cluster') : $t('message.action.primary.storage.scope.zone')) + + '<br><br>' + $t('message.warn.change.primary.storage.scope')"></span> + </template> + </a-alert> + <p></p> + <a-form-item name="clusterid" ref="clusterid" v-if="resource.scope=='ZONE'"> + <template #label> + <tooltip-label :title="$t('label.clustername')" :tooltip="placeholder.clusterid"/> + </template> + <a-select + v-model:value="form.clusterid" + :placeholder="placeholder.clusterid" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" + @change="handleChangeCluster"> + <a-select-option + v-for="cluster in clustersList" + :value="cluster.id" + :key="cluster.id" + :label="cluster.name"> + {{ cluster.name }} + </a-select-option> + </a-select> + </a-form-item> + + <div :span="24" class="action-button"> + <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button> + <a-button @click="handleSubmitForm" ref="submit" type="primary">{{ $t('label.ok') }}</a-button> + </div> + </a-form> + </div> + </div> + </a-spin> +</template> + +<script> +import { ref, reactive, toRaw } from 'vue' +import { api } from '@/api' +import { mixinForm } from '@/utils/mixin' +import DedicateDomain from '../../components/view/DedicateDomain' +import ResourceIcon from '@/components/view/ResourceIcon' +import TooltipLabel from '@/components/widgets/TooltipLabel' + +export default { + name: 'ChangeStoragePoolScope', + mixins: [mixinForm], + components: { + DedicateDomain, + ResourceIcon, + TooltipLabel + }, + props: { + resource: { + type: Object, + required: true + } + }, + data () { + return { + loading: false, + clustersList: [], + selectedCluster: null, + placeholder: { + clusterid: null + } + } + }, + created () { + this.initForm() + this.fetchData() + }, + methods: { + initForm () { + this.formRef = ref() + this.form = reactive({ }) + this.rules = reactive({ + clusterid: [{ required: true, message: this.$t('message.error.select') }] + }) + }, + fetchData () { + this.fetchClusters(this.resource.zoneid) + }, + fetchClusters (zoneId) { + this.form.clusterid = null + this.clustersList = [] + if (!zoneId) return + this.zoneId = zoneId + this.loading = true + api('listClusters', { zoneid: zoneId }).then(response => { + this.clustersList = response.listclustersresponse.cluster || [] + this.form.clusterid = this.clustersList[0].id || null + if (this.form.clusterid) { + this.handleChangeCluster(this.form.clusterid) + } + }).catch(error => { + this.$notifyError(error) + this.clustersList = [] + this.form.clusterid = null + }).finally(() => { + this.loading = false + }) + }, + handleChangeCluster (value) { + this.form.clusterid = value + this.selectedCluster = this.clustersList.find(i => i.id === this.form.clusterid) + }, + handleSubmitForm () { + if (this.loading) return + this.formRef.value.validate().then(() => { + const formRaw = toRaw(this.form) + const values = this.handleRemoveFields(formRaw) + + this.args = {} + if (this.resource.scope === 'ZONE') { + this.args = { + id: this.resource.id, + scope: 'CLUSTER', + clusterid: values.clusterid + } + } else { + this.args = { + id: this.resource.id, + scope: 'ZONE' + } + } + + this.changeStoragePoolScope(this.args) + }).catch(error => { + this.formRef.value.scrollToField(error.errorFields[0].name) + }) + }, + closeAction () { + this.$emit('close-action') + }, + changeStoragePoolScope (args) { + api('changeStoragePoolScope', args).then(json => { + this.$pollJob({ + jobId: json.changestoragepoolscoperesponse.jobid, + title: this.$t('message.success.change.scope'), + description: args.name, + successMessage: this.$t('message.success.change.scope'), + successMethod: (result) => { + this.closeAction() + }, + errorMessage: this.$t('message.change.scope.failed'), + loadingMessage: this.$t('message.change.scope.processing'), + catchMessage: this.$t('error.fetching.async.job.result') + }) + this.closeAction() + }).catch(error => { + this.$notifyError(error) + }).finally(() => { + this.loading = false + }) + } + + } +} +</script> + +<style lang="scss"> + .form { + &__label { + margin-bottom: 5px; + + .required { + margin-left: 10px; + } + } + &__item { + margin-bottom: 20px; + } + .ant-select { + width: 85vw; + @media (min-width: 760px) { + width: 400px; + } + } + } + + .required { + color: #ff0000; + &-label { + display: none; + &--error { + display: block; + } + } + } +</style>